dsh-plugin-tool-management 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -291,12 +291,29 @@ button.dsm-tag:disabled{cursor:default;opacity:.6}
291
291
  .dsm-adv-caret{flex:none;color:var(--dsw-alias-label-tertiary);font-size:10px}
292
292
  /* 「高级选项」永不折行:它在收起时与两行摘要并排,被挤成两字一行就不再像个标题了。 */
293
293
  .dsm-adv-title{flex:none;white-space:nowrap}
294
- /* 折叠摘要固定两行(模型/思考强度/目录注入 + 工具名单),靠 align-items:center 与标题垂直居中。 */
294
+ /* 折叠摘要最多两行(第一行模型 / 思考强度 / 允许追问,第二行工具限制;没有值的那一行不出现),
295
+ 靠 align-items:center 与标题垂直居中。 */
295
296
  .dsm-adv-sum{display:flex;min-width:0;flex-direction:column;gap:1px}
296
- .dsm-adv-body{display:flex;flex-direction:column;gap:12px;padding:11px;border-top:1px solid var(--dsw-alias-border-l1)}
297
+ .dsm-adv-body{display:flex;flex-direction:column;gap:16px;padding:11px;border-top:1px solid var(--dsw-alias-border-l1)}
298
+ /* 分组(0.19.0 重排):组标题 + 一条横贯的细线,组间距靠 .dsm-adv-body 的 16px。
299
+ 重排前是五组平铺、只靠 12px gap 分隔,视觉上连成一片 —— 分组是这次清理的主手段。 */
300
+ .dsm-adv-group{display:flex;flex-direction:column;gap:10px}
301
+ .dsm-adv-group-head{display:flex;align-items:center;gap:8px;color:var(--dsw-alias-label-primary);font-size:12px;font-weight:570}
302
+ .dsm-adv-group-head::after{content:'';height:1px;flex:1;background:var(--dsw-alias-border-l1)}
303
+ /* 半宽字段:与 .dsm-field-row 的列宽同源(calc(50% - 6px) = 12px gap 的一半),
304
+ 所以单独一个半宽字段和并排两个的列边界对齐。 */
305
+ .dsm-adv-half{width:calc(50% - 6px)}
306
+ /* 下拉在 .dsm-combo-row 里必须自己撑满整格。缺这条时 .dsm-select 是 flex:0 1 auto、宽度
307
+ 由**选项文字**撑出:收窄来源后标签变短(去掉了「来源 · 」前缀、候选只剩几条),下拉当场
308
+ 跟着缩 —— 用户 2026-10-07 截图「模型来源选择后,模型框变短了」。实测(headless Chrome,
309
+ 字段宽 251):标签带前缀 251 / 去前缀 251 / 只剩 id 162 / 只有一个模型 135,加了这条全部 251。
310
+ min-width:0 是配套的:flex 项默认 min-width:auto = min-content,不放宽就压不到整格以下。 */
311
+ .dsm-combo-row .dsm-select{flex:1;min-width:0}
297
312
  .dsm-adv-note{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}
298
313
  /* ── 工具限制四行(人设表单:一行一个 Agent 预设,白/黑名单互斥、默认折叠)──────── */
299
- .dsm-modes{display:flex;flex-direction:column;gap:6px;margin-top:6px}
314
+ /* 无 margin-top:这个名单原先紧跟在自己的字段 label 后面,靠 6px 拉开;0.19.0 重排后
315
+ label 撤掉、由分组头承担,间距交给 .dsm-adv-group 的 gap。 */
316
+ .dsm-modes{display:flex;flex-direction:column;gap:6px}
300
317
  .dsm-mode-row{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}
301
318
  .dsm-mode-row.dsm-mode-on{border-color:var(--dsw-alias-border-l3)}
302
319
  .dsm-mode-head{display:flex;min-height:38px;align-items:center;gap:9px;padding:0 10px}
@@ -469,6 +486,15 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
469
486
  .dsm-export-modal .dsm-export-form{display:flex;min-height:0;flex:1;flex-direction:column;gap:10px}
470
487
  .dsm-export-modal .dsm-pick-list{flex:1;min-height:0;max-height:none;overflow:auto}
471
488
  .dsm-export-modal .dsm-export-body{display:flex;min-height:0;flex:1;flex-direction:column;gap:6px}
489
+ /* 会话导出弹窗(0.18.5):字段压成三行之后,560×640 里会话列表只剩几行(用户反馈)。
490
+ 处方照 .dsm-modal-archive / .dsm-modal-fixed:固定高、只有列表内滚。宽度取 720 —— 与
491
+ .dsm-modal-lg / .dsm-modal-wide 同一档,两列字段各约 340px 刚好放得下(880 试过,标题行
492
+ 右边空成一片,用户说太宽)。高度取 760 而不是视口百分比 —— 760 既是 .dsm-modal 自己的
493
+ max-height 也是 .dsm-modal-fixed 那个数,窗口矮时 calc(100vh - 48px) 照常接管。
494
+ 写成两段类名(.dsm-modal.dsm-modal-export-lg)是为了**不靠书写顺序**压过 .dsm-modal-list
495
+ 的 !important(.dsm-modal-lg 排在 .dsm-modal-list 前面,直接借用会被它盖掉)。只加在这一颗
496
+ 弹窗上:.dsm-modal-list 还被四个页面的导出弹窗和回收站共用,动它就是它们一起变大。 */
497
+ .dsm-modal.dsm-modal-export-lg{width:min(720px,100%)!important;height:min(760px,calc(100vh - 48px))!important;max-height:none!important}
472
498
  .dsm-pick-group{display:flex;flex-direction:column}
473
499
  .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}
474
500
  .dsm-pick-group-head:hover{color:var(--dsw-alias-label-primary)}
@@ -3359,7 +3385,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3359
3385
  "source.external.note": "只读接入,启停不改写源文件", "source.dsh.note": "可创建、导入、移到回收站;默认来源必须读取,不能移除或停用",
3360
3386
  "detail.title": "技能详情", "detail.body": "正文", "detail.frontmatter": "元数据", "detail.noFrontmatter": "该技能未提供元数据。", "detail.diagnostics": "诊断", "detail.path": "源文件", "detail.noIssues": "未发现诊断问题。",
3361
3387
  "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 建的是用户级技能",
3362
- "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}",
3388
+ "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}",
3363
3389
  // F-029:服务端错误码补词条(zh 逐字复现服务端文案,命中词条前后中文显示不变;EN 不再回退中文原文)。
3364
3390
  "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}",
3365
3391
  // 与宿主 http-fence.ts 的 TOKEN_MSG **逐字相同**:同一件事在哪个页面出现都必须说同一句话,
@@ -3798,7 +3824,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3798
3824
  "compat.live.state.child": "不在本会话注入",
3799
3825
  "compat.live.state.error": "取数失败",
3800
3826
  "compat.live.state.error.title": "这个域这一轮取数抛了异常(内容还在,是这一步没取到)—— 兼容页「上下文注入通道」有一行降级写着原因。它不会误发「已清空」,下一步会自动重试。",
3801
- "compat.live.state.child.title": "本会话的深度超过了人设的目录注入深度(catalogDepth),所以常驻目录不在这里注入。要让目录出现在子会话,把对应人设的「目录注入」调大(或选「不限制嵌套」)。注意:这不影响委派 —— 子代理始终可以继续委派。",
3827
+ "compat.live.state.child.title": "本会话是子代理会话,这个域对子会话不成立 —— 两种情形:① **场景与记忆**只在顶层注入(它们说的是父会话的处境,子代理要什么由父代理写进委派说明);② **人设目录**的注入深度不够(本会话深度超过该人设的 `catalogDepth`,默认 1 = 只在顶层),这一种可以把人设的「目录注入」调大或选「不限制嵌套」。两种都不影响委派 —— 子代理始终可以继续委派。",
3802
3828
  "compat.live.adopt": "注入 {injected} 次 · 调用 {used} 次 · 采纳 {adopted} 次",
3803
3829
  "compat.live.adopt.none": "注入 {injected} 次 · 从未调用",
3804
3830
  "compat.live.adopt.title": "只数**最近这一段对话**(换一段重新数,重启归零)。采纳 = 调用发生时该域正文正在这个会话的上下文里;调用次数不区分成功失败",
@@ -3909,15 +3935,32 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3909
3935
  "subagents.create": "新建人设", "subagents.edit": "编辑人设",
3910
3936
  "subagents.field.name": "人设名",
3911
3937
  "subagents.field.description": "描述", "subagents.field.description.placeholder": "例如 擅长 Java 后端实现与重构",
3912
- "subagents.field.model": "模型", "subagents.field.model.placeholder": "自定义模型 id", "subagents.field.model.hint": "留空继承主会话", "subagents.field.provider": "模型来源", "subagents.field.provider.placeholder": "deepseek", "subagents.field.provider.hint": "「模型来源」与「模型」是一对:只填模型会落在主会话的来源上,跨来源会解析失败。",
3913
- "subagents.field.catalogDepth": "目录注入", "subagents.field.catalogDepth.onlyTop": "只在顶层", "subagents.field.catalogDepth.toChild": "顶层和子会话", "subagents.field.catalogDepth.toGrand": "顶层和两层子会话", "subagents.field.catalogDepth.unlimited": "不限制嵌套", "subagents.field.catalogDepth.hint": "决定人设目录注入到哪几层会话。\n只影响目录注入,不影响委派 —— 子代理始终可以继续委派。\n子会话看不到目录时,可用 subagent_manager_list 查询。",
3914
- // 思考强度(0.14.0)。界面上只留"这一格现在能选什么",不解释机制 ——
3915
- // 官方语义(改 provider/model 会丢掉继承值、不支持的档位直接拒)记在 CHANGELOG / update.md,
3916
- // 不搬进表单。
3917
- "subagents.field.effort": "思考强度", "subagents.effort.default": "默认(模型的默认强度)", "subagents.effort.none": "默认", "subagents.effort.needModel": "当前默认主会话思考强度", "subagents.effort.loading": "正在问该模型支持哪些档位…", "subagents.effort.unavailable": "该模型没有暴露可选档位,默认思考强度", "subagents.effort.hint": "当前可选择多种思考强度", "subagents.effort.dropped": "换模型后「{effort}」不在新模型的档位里,已清空。",
3918
- "subagents.model.inherit": "继承主会话(不指定)", "subagents.model.customOption": "自定义 / 目录里没有…",
3919
- "subagents.field.modes": "工具限制(按 Agent 预设)",
3920
- "subagents.field.modes.hint": "只有当前模式那一行生效,其他模式用该模式的全部工具。",
3938
+ // 模型来源 / 模型 / 目录注入三格的静态说明(provider.hint、catalogDepth.hint)随 0.19.0
3939
+ // 重排移出表单:来源与模型并排成一组,谁跟谁是一对由位置表达;目录注入那条只说了半句
3940
+ // "不影响委派",半句不解释"那影响什么",留着比不留更费解。
3941
+ "subagents.field.model": "模型", "subagents.field.provider": "模型来源",
3942
+ "subagents.field.offCatalog": "{id} · 目录里没有",
3943
+ "subagents.field.catalogDepth": "目录注入", "subagents.field.catalogDepth.onlyTop": "只在顶层", "subagents.field.catalogDepth.toChild": "顶层和子会话", "subagents.field.catalogDepth.toGrand": "顶层和两层子会话", "subagents.field.catalogDepth.unlimited": "不限制嵌套",
3944
+ // 允许追问(0.19.0)。界面只留复选框副标题那一行 —— 机制(官方 startContinuable / 结算通知 /
3945
+ // 返回指引)与代价(占用可继续子代理名额、需要持久化与 session query)留在 CHANGELOG 与
3946
+ // README(2026-10-07 用户裁定:高级选项里的提示句太密,长说明一律不进表单)。
3947
+ "subagents.field.continuable": "允许追问(后台运行)", "subagents.field.continuable.short": "回执一个 id,之后还能继续对话",
3948
+ // 收起态摘要用的两个「尾巴」。分隔号写在值里(而不是在代码里 join ';'):英文侧要用
3949
+ // "; ",中文用「;」,写进词典才不用为语言改代码。空串 = 这一项没值,整段不出现。
3950
+ "subagents.adv.continuable": ";允许追问(后台运行)",
3951
+ "subagents.adv.legacy": ";旧格式:白名单 {allow} / 黑名单 {deny}",
3952
+ // 思考强度(0.14.0)。界面上只留"这一格现在能选什么"与**状态**(未选模型 / 拉取中 / 拉不到),
3953
+ // 不解释机制 —— 官方语义(改 provider/model 会丢掉继承值、不支持的档位直接拒)记在
3954
+ // CHANGELOG / update.md,不搬进表单。needModel 保留但压到最短:它解释的是"这一格为什么灰着"。
3955
+ "subagents.field.effort": "思考强度", "subagents.effort.default": "默认(模型的默认强度)", "subagents.effort.none": "默认", "subagents.effort.needModel": "先选模型", "subagents.effort.loading": "正在问该模型支持哪些档位…", "subagents.effort.unavailable": "该模型没有暴露可选档位,默认思考强度", "subagents.effort.dropped": "换模型后「{effort}」不在新模型的档位里,已清空。",
3956
+ // 来源下拉的空选项。措辞刻意与模型那条不同("继承主会话"):两者含义一致,但并排两个
3957
+ // 下拉出现同一句话会让人以为点错了格。这一句同时接住被撤掉的那条静态说明
3958
+ // (provider.hint「只填模型会落在主会话的来源上」)要说的事。
3959
+ "subagents.provider.inherit": "不指定(用主会话的来源)",
3960
+ "subagents.model.inherit": "继承主会话(不指定)",
3961
+ // 标题改由高级选项的分组头承担(subagents.adv.group.tools),字段自己的 label 撤掉 ——
3962
+ // 同一句话在 10px 内说两遍("工具限制" + "工具限制(按 Agent 预设)")。
3963
+ "subagents.field.modes.hint": "只有当前模式那一行生效;MCP 工具不在候选里,始终可用",
3921
3964
  "subagents.mode.startAllow": "启动白名单", "subagents.mode.startDeny": "启动黑名单", "subagents.mode.stopAllow": "关闭白名单", "subagents.mode.stopDeny": "关闭黑名单",
3922
3965
  "subagents.mode.off": "未限制", "subagents.mode.empty": "已启动但没勾选(= 不限制)",
3923
3966
  "subagents.mode.allowOn": "白名单 {count} 个", "subagents.mode.denyOn": "黑名单 {count} 个",
@@ -3930,8 +3973,10 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3930
3973
  "subagents.legacy.note": "这个文件里还有一份旧写法的工具限制(白名单 {allow} 个 / 黑名单 {deny} 个),它对所有模式都生效。",
3931
3974
  "subagents.legacy.convert": "转换到按模式设置",
3932
3975
  "subagents.legacy.hint": "转换会把旧名单搬进你选的模式;不点它,文件里的旧键原样保留(运行时照旧生效)。",
3933
- "subagents.tools.filter": "筛选工具名", "subagents.tools.remove": "移除", "subagents.toolFailed": "子代理工具没注册上:{names}({reason})。这几个工具在子智能体流程里用不了;重启 DSH 后仍如此请核对宿主版本。",
3934
- "subagents.adv.title": "高级选项", "subagents.adv.inherit": "继承主会话", "subagents.adv.summary": "模型:{model};思考强度:{effort};目录注入:{depth}", "subagents.adv.summary2": "模式限制:{modes} 项;旧格式:白名单 {allow} / 黑名单 {deny}", "subagents.adv.note": "白名单 = 子代理只能用勾选的工具。\n黑名单 = 除勾选的以外都能用。\nMCP 工具不在候选里:子代理始终能用当前在跑的 MCP。", "subagents.adv.loadFailed": "读取候选数据失败(模型目录 / 工具清单);仍可手动填写。",
3976
+ "subagents.tools.filter": "筛选工具名", "subagents.tools.remove": "移除", "subagents.tools.group.plugin": "插件工具", "subagents.tools.group.official": "官方工具", "subagents.tools.group.other": "其他工具", "subagents.toolFailed": "子代理工具没注册上:{names}({reason})。这几个工具在子智能体流程里用不了;重启 DSH 后仍如此请核对宿主版本。",
3977
+ // 收起态摘要最多两行、只报有值的项:第一行模型 / 思考强度(+ 允许追问),第二行工具限制。
3978
+ // 目录注入不进摘要 —— 它只决定目录出现在哪些会话,不影响委派本身。
3979
+ "subagents.adv.title": "高级选项", "subagents.adv.inherit": "继承主会话", "subagents.adv.summary": "模型:{model};思考强度:{effort}{tail}", "subagents.adv.summary2": "模式限制:{modes} 项{tail}", "subagents.adv.loadFailed": "读取候选数据失败(模型目录 / 工具清单);仍可手动填写。", "subagents.adv.group.model": "模型", "subagents.adv.group.runtime": "运行方式", "subagents.adv.group.tools": "工具限制",
3935
3980
  "preset.name.standard": "标准模式", "preset.name.ptc": "PTC模式", "preset.name.minimal": "极简模式", "preset.name.cordis": "创造模式",
3936
3981
  "preset.short.standard": "标准", "preset.short.ptc": "PTC", "preset.short.minimal": "极简", "preset.short.cordis": "创造",
3937
3982
  "subagents.field.body": "人设提示词", "subagents.field.body.placeholder": "写下这个人设的身份、职责与工作方式…",
@@ -3949,7 +3994,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3949
3994
  "import.none": "没有导入任何文件(全部被跳过)", "import.skipped": "已跳过:{items}",
3950
3995
  "error.import.noFiles": "没有选择要导入的文件",
3951
3996
  "nav.title": "工具",
3952
- "agm.stat.total": "个全局提示词", "agm.stat.active": "个生效中", "agm.loading": "加载中…", "agm.empty": "暂无提示词,点「新增提示词」创建", "agm.empty.search": "没有匹配的提示词", "agm.search.placeholder": "搜索提示词 id 或描述", "agm.btn.delete.blocked.refs": "不能删除:{refs}。先改掉引用(换绑提示词 / 退出场景 / 应用别的预设)再删除。", "agm.ref.sep": ";", "agm.ref.scene": "场景「{scene}」绑定了它", "agm.ref.sceneActive": "场景「{scene}」(已启用)绑定了它", "agm.ref.file": "AGENTS.md 当前内容就是它", "agm.ref.restore": "退出场景后要恢复的全局提示词", "agm.sceneLock.notice": "场景「{scene}」接管中:当前生效的是它绑定的「{id}」。在这里应用别的预设 = 把该场景改绑到那一份(会同步写入场景档案)。", "agm.active": "生效中", "agm.active.hint.scene": "当前生效:场景「{scene}」绑定的提示词预设", "agm.active.hint.file": "当前生效:AGENTS.md 就是这一份", "agm.file.applied": "文件里是它", "agm.file.applied.hint": "AGENTS.md 当前内容是它;启用的场景绑了别的提示词,切换场景时已按场景把文件改成那一份", "agm.apply.hint": "应用后写入 AGENTS.md;宿主每轮重读该文件,下一轮对话生效", "agm.apply.switch": "应用提示词", "agm.apply.switch.on": "当前生效的就是这一份 · 点一下重新写回 AGENTS.md(撤销手动改动)", "agm.applyModal.title": "应用预设", "agm.edit.title": "编辑预设", "agm.remove.title": "删除预设", "agm.btn.edit": "编辑", "agm.btn.delete": "删除", "agm.btn.new": "新增提示词", "agm.btn.import": "导入提示词", "agm.result.imported": "已导入 {count} 份预设:{names}", "agm.btn.save": "保存", "agm.btn.create": "创建", "agm.btn.confirmRemove": "确认删除", "agm.btn.confirmApply": "确认应用", "agm.field.id": "id(目录名)", "agm.field.id.hint": "中文、空格、点都可以;不能含斜杠、反斜杠与 : * ? < > 等符号;≤64 字符。改 id 等于目录改名。", "agm.field.id.placeholder": "例如:工作基线", "agm.field.content": "内容", "agm.field.content.placeholder": "直接写这份预设的正文(Markdown);留空 = 用空白模板", "agm.field.desc": "描述(模型在清单里看得到)", "agm.field.desc.placeholder": "这份预设是干什么的、什么时候用它", "agm.result.renamed": "已保存,id 改为「{id}」", "agm.result.rebound": "(同步了 {count} 个场景的绑定)", "agm.remove.suffix": " ?此操作不可恢复。", "agm.apply.prefix": "将把 ", "agm.apply.suffix": " 的内容写入 ~/.dsh/AGENTS.md。", "agm.apply.note": "下一轮对话生效;当前内容已备份到 __last-applied__(保留最近 5 代)", "hist.err.load": "加载失败", "hist.err.unarchive": "恢复失败", "hist.err.delete": "删除失败", "hist.err.retention": "设置失败", "hist.err.import": "导入失败", "hist.err.sessions": "加载会话列表失败", "hist.err.export": "导出失败", "hist.err.archive": "归档失败", "hist.retention.forever": "永久保留", "hist.retention.days": "{count} 天", "hist.arch.all": "全部会话", "hist.arch.archived": "仅已归档", "hist.arch.live": "仅未归档", "hist.arch.missing": "仅目录丢失", "hist.ws.all": "全部工作区", "hist.ungrouped": "未分组", "hist.group.removed": "已移除", "hist.group.removed.hint": "该目录当前在 DSH 里没有工作区登记;分组按会话目录重建,重新登记后会自动并回同一组", "hist.group.dirMissing": "目录已不存在", "hist.group.dirMissing.hint": "工作区目录已从磁盘删除,无法重新登记;组内会话仍可恢复(归入未分组)或永久删除", "hist.btn.registerWs": "重新登记", "hist.btn.registerWs.hint": "重新登记这个目录,并把该目录下已有的会话挂回该组(只写工作区记账,不移动、不删除文件与会话)", "hist.register.title": "重新登记?", "hist.register.body": "将为「{title}」在 DSH 中重新创建一条工作区登记,并把该目录下已有的会话挂回这个工作区(恢复其分组)。只写工作区记账,不移动、不删除任何文件与会话。", "hist.register.confirm": "确认登记", "hist.register.done.title": "已重新登记", "hist.register.done.body": "工作区「{title}」已登记;该目录下的归档会话会在刷新后并入这一组。", "hist.register.done.attached": "已把该目录下 {count} 个已有会话挂回这个工作区。", "hist.register.done.skipped": "{count} 个会话未能挂回:{reason}", "hist.err.register": "登记失败", "scenes.field.prompt": "提示词预设", "scenes.agents.note.applied": "已把「{id}」写入 AGENTS.md", "scenes.agents.note.restored": "已恢复进场景前的 AGENTS.md", "scenes.agents.note.error": "AGENTS.md 未写入:{reason}", "scenes.agents.note.baselineLost": "⚠ 退出场景时将恢复不回原来的 AGENTS.md:{reason}", "scenes.field.prompt.hint": "为当前场景设置全局提示词", "scenes.prompt.activeTag": "生效中", "scenes.prompt.noPresets": "还没有提示词预设;可先去「提示词」页新建一个。", "scenes.prompt.bound": "提示词:{id}", "scenes.prompt.live": "提示词生效中:{id}", "scenes.prompt.missing": "预设不存在:{id}", "scenes.prompt.tag.hint": "这个场景绑定的提示词预设", "scenes.prompt.mismatch.hint": "场景绑定的「{id}」与 AGENTS.md 当前内容不一致(可能被手动改过);到提示词页对它点「重新应用」写回", "scenes.enable.blocked": "已启用场景「{name}」;先关掉它才能启用别的", "scenes.result.created.collapsed": "场景「{name}」已创建,默认不启动(原先的「全部启用」已收敛为单选)", "scenes.legacyAll": "检测到 {count} 个场景同时处于启用状态(历史「全部启用」遗留);除「全局」外同时只能启用一个。", "scenes.legacyAll.fix": "收敛为单选", "scenes.preview.title": "进入场景「{name}」前,先看一眼会改什么", "scenes.preview.loading": "正在计算这次会改什么…", "scenes.preview.failed": "预览没算出来:{error}", "scenes.preview.enter": "确认进入", "scenes.preview.dismiss": "不再显示", "scenes.preview.dismiss.title": "关闭这张卡,并且以后进入场景前不再弹(想恢复用页面右上角的「开启提醒」)", "scenes.preview.toggle.off": "关闭提醒", "scenes.preview.toggle.on": "开启提醒", "scenes.preview.toggle.title": "进入场景前先弹一张「会改什么」的卡", "scenes.preview.nochange": "什么都不用改:当前运行时已经和这个场景要的一致了。", "scenes.preview.noArchive": "这个场景还没保存过档案 —— MCP / 技能 / 人设三个域都按「一个都没勾」处理,进入即全部停用。", "scenes.preview.exitFirst": "先退出「{scene}」:它改过的东西按进场景前的快照还原。", "scenes.preview.exitNoSnapshot": "当前在场景「{scene}」里,但进场景前的快照已丢失:退出只能清掉模式标记,运行时回不到原样。", "scenes.preview.row.serversOn": "启用 {count} 台 MCP 服务器:{names}", "scenes.preview.row.serversOff": "停用 {count} 台 MCP 服务器:{names}", "scenes.preview.row.toolsOn": "放回 {count} 个 MCP 工具:{names}", "scenes.preview.row.toolsOff": "停用 {count} 个 MCP 工具:{names}", "scenes.preview.row.sourcesOn": "启用 {count} 个技能目录:{names}", "scenes.preview.row.sourcesOff": "停用 {count} 个技能目录:{names}", "scenes.preview.row.skillsOn": "启用 {count} 个技能:{names}", "scenes.preview.row.skillsOff": "停用 {count} 个技能:{names}", "scenes.preview.row.personasOn": "启用 {count} 个人设:{names}", "scenes.preview.row.personasOff": "停用 {count} 个人设:{names}", "scenes.preview.row.quickOn": "启用 {count} 条快捷提示词:{names}", "scenes.preview.row.quickOff": "停用 {count} 条快捷提示词:{names}", "scenes.preview.enter.servers": "启用 {count} 台 MCP:{names}", "scenes.preview.enter.memories": "启用 {count} 条记忆:{names}", "scenes.preview.row.notes": "用场景备注覆盖 {count} 台服务器的备注", "scenes.field.toolTable": "模型工具表方案", "scenes.field.toolTable.hint": "为当前场景设置可使用的模型工具方案", "scenes.field.quickPrompts": "快捷提示词", "scenes.field.quickPrompts.current": "当前启用的(保持现状)", "scenes.field.quickPrompts.custom": "按场景勾选", "scenes.field.quickPrompts.none": "还没有快捷提示词,先到「提示词」页新增", "scenes.field.quickPrompts.hint": "为当前场景设置快捷提示词", "scenes.field.quickPrompts.list": "本场景启用", "scenes.form.section.basic": "基本信息", "scenes.form.section.binding": "场景绑定", "scenes.field.toolTable.none": "不绑定(保持现状)", "scenes.field.toolTable.factory": "出厂默认(关 {count} 条)", "scenes.field.toolTable.count": "关 {count} 条", "scenes.field.toolTable.missing": "方案已不存在", "scenes.preview.row.toolPreset": "工具表将切到方案「{name}」(关 {count} 条)", "scenes.preview.row.toolPresetMissing": "工具表绑的方案「{name}」已不存在,这次不切换", "scenes.preview.more": " 等", "scenes.preview.truncated": "(每一类最多列前 60 项)", "scenes.preview.stale": "档案里有 {count} 项在当前环境里已经不存在,这次不会生效:{items}", "scenes.doctor.title": "有 {count} 处引用指向已经不存在的名字", "scenes.doctor.noneButSkipped": "体检没发现悬空引用", "scenes.doctor.hint": "改名或删除留下的:这一条看着还绑着,运行时按那个名字已经找不到东西了。改回正确的名字,或到档案里取消勾选。", "scenes.doctor.row": "{where}:「{name}」已不存在", "scenes.doctor.more": "另有 {count} 处未列出", "scenes.doctor.skipped": "({domains} 这次没读到,这一类没有算进体检)", "agm.restore.tag": "退出场景后恢复它", "agm.restore.tag.hint": "它是进场景前的全局提示词,退出场景时会按它恢复——删了就没法恢复了", "agm.file.mismatch": "文件里不是它", "agm.file.mismatch.hint": "AGENTS.md 当前内容与这份预设不一致(可能被手动改过);点左边那颗开关把它写回", "hist.restore.note.registered": "已恢复 {count} 个会话;工作区「{title}」已重新登记,会话已挂回该组。", "hist.restore.note.attached": "已恢复 {count} 个会话,并挂回工作区「{title}」。", "hist.restore.note.skip": "{count} 个会话的工作区归属未能恢复:{reason}", "hist.purge.title": "永久删除?", "hist.purge.one": "永久删除会话「{title}」及其全部记录,不可恢复", "hist.purge.batch": "永久删除所选 {count} 个会话及其全部记录,不可恢复", "hist.btn.restore": "恢复", "hist.btn.purge": "永久删除", "hist.btn.confirmPurge": "确认删除", "hist.selectGroup": "全选组 {title}", "hist.group.count": "{count} 个", "hist.selectAll": "全选", "hist.deselectAll": "取消全选", "hist.btn.import": "导入会话", "hist.btn.export": "导出会话", "hist.selected": "已选 {count} 项", "hist.btn.restoreSelected": "恢复所选 ({count})", "hist.btn.deleteSelected": "删除所选 ({count})", "hist.stat.archived": "个归档", "hist.stat.projects": "个项目", "hist.stat.retention": "天保留", "hist.search.placeholder": "搜索标题 / 会话 ID / 项目路径", "hist.loading": "加载中…", "hist.empty.search": "无匹配的归档会话", "hist.empty.none": "暂无归档会话", "hist.import.cwd": "项目目录(可留空)", "hist.import.cwd.placeholder": "绝对路径,可留空(会话归入未分组)", "hist.import.drop": "选择或拖入对话文件", "hist.import.done": "已创建会话 {id}({count} 条消息),可在 DSH 会话列表中继续对话。", "hist.import.lossy": "⚠ 整份文本没有识别出任何说话人标记,已折成一条用户消息:原对话的分轮结构可能已丢失。若源文件是 JSONL 或 Markdown 转录稿,请按原扩展名重新导入。", "hist.export.hint": "导出选中会话为转录文件,可再次导入", "hist.export.dir": "导出目录(绝对路径,自动创建)", "hist.export.browse": "浏览并选择文件夹", "hist.btn.browse": "选择", "hist.export.pickDir": "选择导出目录", "hist.export.format": "格式", "hist.export.scope": "会话范围", "hist.export.workspace": "工作区", "hist.export.sessions": "会话({count} 个)", "hist.export.loading": "加载会话列表…", "hist.cwd.missing": "⚠ 工作区目录已不存在:", "hist.tag.archived": "已归档", "hist.tag.live": "未归档", "hist.export.empty": "没有符合条件的会话", "hist.result.exported": "已导出 {count} 个会话到 {dir}", "hist.result.skipped": ";跳过 {count} 个({ids})", "hist.result.archived": "已归档 {count} 个会话到 History,可到 History 页继续管理", "hist.result.archiveFailed": ";其中 {count} 个未能归档", "hist.btn.archiveSelected": "归档所选", "hist.btn.archiveSelected.title": "把选中的会话收进 History,纳入保留期管理", "hist.export.busy": "处理中…", "hist.btn.exportSelected": "导出 {count} 个会话", "hist.error.title": "操作失败", "hist.error.unknown": "未知错误",
3997
+ "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": "未知错误",
3953
3998
  },
3954
3999
  en: {
3955
4000
  "title": "Skills", "desc": "Manage skills: enable, import, create, trash.", "link.project": "GitHub", "link.feedback": "Issues",
@@ -3988,7 +4033,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3988
4033
  "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",
3989
4034
  "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.",
3990
4035
  "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",
3991
- "import.title": "Import skill", "upload.drop.title": "Click or drop here", "upload.drop.copy": "Supported: .zip, a skill folder, or one SKILL.md", "upload.selected.one": "{count} file · {size}", "upload.selected.other": "{count} files · {size}", "upload.remove": "Remove selection", "upload.requirements": "File requirements", "upload.requirement.skill": "Archives and folders must contain SKILL.md", "upload.requirement.frontmatter": "SKILL.md must include a YAML name and description", "upload.requirement.copy": "Import copies all content and never modifies the source", "upload.requirement.persona.1": "Accepts .md / .zip (multi-select or drag in)", "upload.requirement.persona.2": "One .md = one persona; the file name is the persona name", "upload.requirement.persona.3": "Same names are skipped, never overwritten", "upload.requirement.memory.1": "Accepts .md / .zip (multi-select or drag in)", "upload.requirement.memory.2": "One .md = one memory; the file name is the memory name", "upload.requirement.memory.3": "A folder with SKILL.md becomes a bundle memory (attachments included)", "upload.requirement.memory.4": "Folder names inside the zip are scenes; otherwise the scene field below applies (blank = global)", "upload.requirement.prompt.1": "Accepts .md (multi-select or drag in)", "upload.requirement.prompt.2": "One .md = one preset; the file name is the id", "upload.requirement.prompt.3": "A preset with the same name is skipped, never overwritten", "upload.requirement.session.1": "Accepts .jsonl / .json / .md / .txt (one file at a time)", "upload.requirement.session.2": "Transcripts from Claude Code / Cursor / Codex, or any plain text", "upload.requirement.session.3": "The project directory may be left blank (the session goes to Ungrouped)", "upload.importing": "Importing…", "status.selected": "Selected", "select.file.invalid": "Choose a .zip archive or one SKILL.md.", "select.folder.invalid": "No SKILL.md was found in the selected folder.", "error.browse.absolute": "Folder path must be absolute: {path}", "error.browse.unreadable": "Could not read folder: {path}", "error.browse.notDirectory": "Not a folder: {path}",
4036
+ "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}",
3992
4037
  "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}",
3993
4038
  // 逐字对应宿主 http-fence.ts 的 TOKEN_MSG_*(见中文侧注释)。
3994
4039
  "error.token.required": "Missing access token, or the wrong one",
@@ -4417,7 +4462,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
4417
4462
  "compat.live.state.child": "Not injected here",
4418
4463
  "compat.live.state.error": "Failed to load",
4419
4464
  "compat.live.state.error.title": "This domain threw while producing its text this round (the content exists; this step just could not read it). The Compat page's \"Context injection channel\" row carries a degraded note with the reason. No false \"cleared\" notice is sent, and the next step retries automatically.",
4420
- "compat.live.state.child.title": "This session is deeper than the personas' catalog injection depth (catalogDepth), so the standing catalog is not injected here. To let the catalog reach subagent sessions, raise the persona's catalog injection depth (or pick “No nesting limit”). Note: this does not affect delegation — subagents can always delegate further.",
4465
+ "compat.live.state.child.title": "This session is a subagent session and this domain does not apply to it — two cases: (1) **scene and memory** are injected at the top level only (they describe the parent session's situation; what a subagent needs belongs in the delegation brief); (2) the **persona catalog**'s injection depth is too shallow (this session is deeper than that persona's `catalogDepth`, default 1 = top level only) — raise that persona's catalog injection depth or pick “No nesting limit”. Neither affects delegation — subagents can always delegate further.",
4421
4466
  "compat.live.adopt": "injected {injected} · called {used} · adopted {adopted}",
4422
4467
  "compat.live.adopt.none": "injected {injected} · never called",
4423
4468
  "compat.live.adopt.title": "Counts the most recent conversation only (it restarts when you switch, and on every launch). Adopted = the domain text was in context when the call happened; calls are counted whether or not they succeeded",
@@ -4528,15 +4573,37 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
4528
4573
  "subagents.create": "New persona", "subagents.edit": "Edit persona",
4529
4574
  "subagents.field.name": "Persona name",
4530
4575
  "subagents.field.description": "Description", "subagents.field.description.placeholder": "e.g. Senior Java engineer",
4531
- "subagents.field.model": "Model", "subagents.field.model.placeholder": "Custom model id", "subagents.field.model.hint": "Blank inherits the main session", "subagents.field.provider": "Model provider", "subagents.field.provider.placeholder": "deepseek", "subagents.field.provider.hint": "Provider and model are a pair: a model alone resolves against the main session's provider and fails across providers.",
4532
- "subagents.field.catalogDepth": "Catalog injection", "subagents.field.catalogDepth.onlyTop": "Top level only", "subagents.field.catalogDepth.toChild": "Top level and subagents", "subagents.field.catalogDepth.toGrand": "Top level and two levels down", "subagents.field.catalogDepth.unlimited": "No nesting limit", "subagents.field.catalogDepth.hint": "Which levels of sessions get the persona catalog.\nIt affects the catalog only — subagents can always delegate further.\nWhen the catalog is not injected, subagent_manager_list still works.",
4533
- // Reasoning effort (0.14.0). The form only says what this field offers right now; the
4534
- // official semantics (the host drops the inherited level when provider/model change, and an
4535
- // unsupported level is rejected rather than clamped) live in CHANGELOG / update.md, not here.
4536
- "subagents.field.effort": "Reasoning effort", "subagents.effort.default": "Default (the model's default)", "subagents.effort.none": "default", "subagents.effort.needModel": "Currently the main session's default reasoning effort.", "subagents.effort.loading": "Asking the model which levels it supports…", "subagents.effort.unavailable": "This model exposes no selectable levels — default reasoning effort.", "subagents.effort.hint": "Several reasoning-effort levels are available.", "subagents.effort.dropped": "“{effort}” is not among the new model's levels, so it was cleared.",
4537
- "subagents.model.inherit": "Inherit the main session (unspecified)", "subagents.model.customOption": "Custom / not in the catalogue…",
4538
- "subagents.field.modes": "Tool limits (per agent preset)",
4539
- "subagents.field.modes.hint": "Only the row for the current preset applies; other presets keep their full tool set.",
4576
+ // The static help lines for provider / model / catalog depth left the form with the 0.19.0
4577
+ // re-layout: provider and model now sit side by side as one group, so the pairing is carried
4578
+ // by position; the catalog-depth line only stated half a fact ("affects the catalog only")
4579
+ // without saying what it does affect, which is more confusing than saying nothing.
4580
+ "subagents.field.model": "Model", "subagents.field.provider": "Model provider",
4581
+ "subagents.field.offCatalog": "{id} · not in the catalogue",
4582
+ "subagents.field.catalogDepth": "Catalog injection", "subagents.field.catalogDepth.onlyTop": "Top level only", "subagents.field.catalogDepth.toChild": "Top level and subagents", "subagents.field.catalogDepth.toGrand": "Top level and two levels down", "subagents.field.catalogDepth.unlimited": "No nesting limit",
4583
+ // Continuable (0.19.0). The form keeps only the checkbox subtitle; the mechanism (the
4584
+ // official startContinuable, the settlement notice, the return guidance) and the cost
4585
+ // (capacity slot, persistence + session query) live in CHANGELOG and README, not here
4586
+ // (user ruling 2026-10-07: the advanced section carried far too many help sentences).
4587
+ "subagents.field.continuable": "Continuable (runs in the background)", "subagents.field.continuable.short": "Returns an id you can keep talking to",
4588
+ // The two "tails" of the collapsed summary. The separator lives inside the value (not a
4589
+ // hard-coded join in code): English needs "; " where Chinese needs ";". Empty = no value,
4590
+ // so the whole clause disappears.
4591
+ "subagents.adv.continuable": "; continuable (runs in the background)",
4592
+ "subagents.adv.legacy": "; legacy allow: {allow} / deny: {deny}",
4593
+ // Reasoning effort (0.14.0). The form keeps only what this field offers right now plus the
4594
+ // transient states (no model picked / loading / none available); the official semantics (the
4595
+ // host drops the inherited level when provider/model change, and an unsupported level is
4596
+ // rejected rather than clamped) live in CHANGELOG / update.md, not here.
4597
+ "subagents.field.effort": "Reasoning effort", "subagents.effort.default": "Default (the model's default)", "subagents.effort.none": "default", "subagents.effort.needModel": "Pick a model first", "subagents.effort.loading": "Asking the model which levels it supports…", "subagents.effort.unavailable": "This model exposes no selectable levels — default reasoning effort.", "subagents.effort.dropped": "“{effort}” is not among the new model's levels, so it was cleared.",
4598
+ // The blank entry of the provider dropdown. Deliberately worded differently from the model
4599
+ // one ("Inherit the main session"): same meaning, but the same sentence in two adjacent
4600
+ // dropdowns reads like a mis-click. This line also carries what the removed static hint
4601
+ // (provider.hint) used to say.
4602
+ "subagents.provider.inherit": "Unspecified (main session's provider)",
4603
+ "subagents.model.inherit": "Inherit the main session (unspecified)",
4604
+ // The heading moved to the advanced section's group head (subagents.adv.group.tools) and the
4605
+ // field's own label was dropped — the same phrase twice within 10px otherwise.
4606
+ "subagents.field.modes.hint": "Only the current preset's row applies; MCP tools are not listed and always stay available",
4540
4607
  "subagents.mode.startAllow": "Enable allowlist", "subagents.mode.startDeny": "Enable denylist", "subagents.mode.stopAllow": "Disable allowlist", "subagents.mode.stopDeny": "Disable denylist",
4541
4608
  "subagents.mode.off": "no restriction", "subagents.mode.empty": "enabled with nothing picked (= no restriction)",
4542
4609
  "subagents.mode.allowOn": "allowlist {count}", "subagents.mode.denyOn": "denylist {count}",
@@ -4549,8 +4616,11 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
4549
4616
  "subagents.legacy.note": "This file still carries the old-style tool limits (allowlist {allow} / denylist {deny}); they apply to every preset.",
4550
4617
  "subagents.legacy.convert": "Convert into a preset row",
4551
4618
  "subagents.legacy.hint": "Converting moves the old list into the preset you pick; until then the old keys stay untouched in the file (and keep working at run time).",
4552
- "subagents.tools.filter": "Filter tool names", "subagents.tools.remove": "Remove", "subagents.toolFailed": "Subagent tools failed to register: {names} ({reason}). They cannot be used in the subagent flow; if this persists after a DSH restart, check the host version.",
4553
- "subagents.adv.title": "Advanced options", "subagents.adv.inherit": "inherit", "subagents.adv.summary": "model: {model}; effort: {effort}; catalog: {depth}", "subagents.adv.summary2": "preset limits: {modes}; legacy allow: {allow} / deny: {deny}", "subagents.adv.note": "Allowlist = the subagent may only use the picked tools.\nDenylist = everything except the picked ones.\nMCP tools are not listed: a subagent always keeps the MCP servers currently running.", "subagents.adv.loadFailed": "Could not read the candidate data (model catalogue / tool list); you can still fill the fields by hand.",
4619
+ "subagents.tools.filter": "Filter tool names", "subagents.tools.remove": "Remove", "subagents.tools.group.plugin": "Plugin tools", "subagents.tools.group.official": "Official tools", "subagents.tools.group.other": "Other tools", "subagents.toolFailed": "Subagent tools failed to register: {names} ({reason}). They cannot be used in the subagent flow; if this persists after a DSH restart, check the host version.",
4620
+ // The collapsed summary is at most two lines and reports only what has a value: line 1 is
4621
+ // model / effort (+ continuable), line 2 is the tool limits. Catalog injection is left out —
4622
+ // it only decides which sessions see the catalogue, not how a delegation runs.
4623
+ "subagents.adv.title": "Advanced options", "subagents.adv.inherit": "inherit", "subagents.adv.summary": "model: {model}; effort: {effort}{tail}", "subagents.adv.summary2": "preset limits: {modes}{tail}", "subagents.adv.loadFailed": "Could not read the candidate data (model catalogue / tool list); you can still fill the fields by hand.", "subagents.adv.group.model": "Model", "subagents.adv.group.runtime": "Runtime", "subagents.adv.group.tools": "Tool limits",
4554
4624
  "preset.name.standard": "Standard mode", "preset.name.ptc": "PTC mode", "preset.name.minimal": "Minimal mode", "preset.name.cordis": "Creator mode",
4555
4625
  "preset.short.standard": "Standard", "preset.short.ptc": "PTC", "preset.short.minimal": "Minimal", "preset.short.cordis": "Creator",
4556
4626
  "subagents.field.body": "Persona prompt", "subagents.field.body.placeholder": "Describe the persona's role, responsibilities, and working style…",
@@ -4568,7 +4638,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
4568
4638
  "import.none": "Nothing was imported (all skipped)", "import.skipped": "Skipped: {items}",
4569
4639
  "error.import.noFiles": "No files selected",
4570
4640
  "nav.title": "Tools",
4571
- "agm.stat.total": " global prompts", "agm.stat.active": " active", "agm.loading": "Loading…", "agm.empty": "No prompts yet — click “New prompt” to create one", "agm.empty.search": "No matching prompts", "agm.search.placeholder": "Search prompt ids or descriptions", "agm.btn.delete.blocked.refs": "Cannot delete: {refs}. Change those references first (rebind the prompt, leave the scene, or apply another preset).", "agm.ref.sep": "; ", "agm.ref.scene": "bound by scene “{scene}”", "agm.ref.sceneActive": "bound by scene “{scene}” (enabled)", "agm.ref.file": "this is what AGENTS.md currently holds", "agm.ref.restore": "the global prompt to restore after leaving the scene", "agm.sceneLock.notice": "Scene “{scene}” is driving the baseline: the prompt in effect is its bound “{id}”. Applying another preset here rebinds that scene to it (written into the scene profile as well).", "agm.active": "Active", "agm.active.hint.scene": "Currently active: the prompt preset bound to scene “{scene}”", "agm.active.hint.file": "Currently active: AGENTS.md is exactly this preset", "agm.file.applied": "In AGENTS.md", "agm.file.applied.hint": "AGENTS.md currently holds this one; the enabled scene binds a different preset, which now takes effect", "agm.apply.hint": "Writes AGENTS.md; the host re-reads that file every turn, so it takes effect on the next turn", "agm.apply.switch": "Apply prompt", "agm.apply.switch.on": "This one is in effect · click to write AGENTS.md again (undoing hand edits)", "agm.applyModal.title": "Apply preset", "agm.edit.title": "Edit preset", "agm.remove.title": "Delete preset", "agm.btn.edit": "Edit", "agm.btn.delete": "Delete", "agm.btn.new": "New prompt", "agm.btn.import": "Import prompts", "agm.result.imported": "Imported {count} preset(s): {names}", "agm.btn.save": "Save", "agm.btn.create": "Create", "agm.btn.confirmRemove": "Delete", "agm.btn.confirmApply": "Apply", "agm.field.id": "id (directory name)", "agm.field.id.hint": "Letters, spaces and dots are fine; no slashes or : * ? < >; up to 64 chars. Changing it renames the directory.", "agm.field.id.placeholder": "e.g. work-baseline", "agm.field.content": "Content", "agm.field.content.placeholder": "Write the preset body (Markdown) here; leave blank to start from the empty template", "agm.field.desc": "Description (the model sees this in the list)", "agm.field.desc.placeholder": "What this preset is for and when to use it", "agm.result.renamed": "Saved; id is now “{id}”", "agm.result.rebound": " ({count} scene binding(s) updated)", "agm.remove.suffix": "? This cannot be undone.", "agm.apply.prefix": "Write the contents of ", "agm.apply.suffix": " into ~/.dsh/AGENTS.md.", "agm.apply.note": "Takes effect on the next turn; the current content is backed up to __last-applied__ (last 5 kept)", "hist.err.load": "Load failed", "hist.err.unarchive": "Restore failed", "hist.err.delete": "Delete failed", "hist.err.retention": "Update failed", "hist.err.import": "Import failed", "hist.err.sessions": "Failed to load sessions", "hist.err.export": "Export failed", "hist.err.archive": "Archive failed", "hist.retention.forever": "Keep forever", "hist.retention.days": "{count} days", "hist.arch.all": "All sessions", "hist.arch.archived": "Archived only", "hist.arch.live": "Not archived only", "hist.arch.missing": "Missing directory only", "hist.ws.all": "All workspaces", "hist.ungrouped": "Ungrouped", "hist.group.removed": "Removed", "hist.group.removed.hint": "This directory has no DSH workspace registration right now; the group is rebuilt from session directories and rejoins automatically once registered again", "hist.group.dirMissing": "Directory missing", "hist.group.dirMissing.hint": "The workspace directory was deleted from disk, so it cannot be registered again; sessions in this group can still be restored (they join Ungrouped) or deleted forever", "hist.btn.registerWs": "Register", "hist.btn.registerWs.hint": "Register this directory again and attach its existing sessions back to that group (workspace accounting only — no file or session is moved or deleted)", "hist.register.title": "Register again?", "hist.register.body": "A workspace registration will be created in DSH for “{title}”, and the sessions under this directory will be attached back to it (restoring their grouping). This writes workspace accounting only — no file or session is moved or deleted.", "hist.register.confirm": "Register", "hist.register.done.title": "Registered", "hist.register.done.body": "Workspace “{title}” is registered; archived sessions under this directory join that group after a refresh.", "hist.register.done.attached": "Attached {count} existing session(s) under this directory back to the workspace.", "hist.register.done.skipped": "{count} session(s) could not be attached: {reason}", "hist.err.register": "Registration failed", "scenes.field.prompt": "Prompt preset", "scenes.agents.note.applied": "wrote “{id}” into AGENTS.md", "scenes.agents.note.restored": "restored the AGENTS.md baseline from before the scene", "scenes.agents.note.error": "AGENTS.md was not written: {reason}", "scenes.agents.note.baselineLost": "⚠ Leaving the scene will not restore the previous AGENTS.md: {reason}", "scenes.field.prompt.hint": "Sets the global prompt for this scene", "scenes.prompt.activeTag": "in effect", "scenes.prompt.noPresets": "No prompt presets yet — create one on the Prompts page.", "scenes.prompt.bound": "Prompt: {id}", "scenes.prompt.live": "Prompt active: {id}", "scenes.prompt.missing": "Preset missing: {id}", "scenes.prompt.tag.hint": "The prompt preset bound to this scene", "scenes.prompt.mismatch.hint": "The scene-bound “{id}” does not match the current contents of AGENTS.md (it may have been edited by hand); click “Apply again” for it on the Prompts page to write it back", "scenes.enable.blocked": "Scene “{name}” is already enabled; turn it off first", "scenes.result.created.collapsed": "Scene “{name}” created and left off (the legacy “all enabled” default collapsed to a single choice)", "scenes.legacyAll": "{count} scenes are enabled at once (a leftover of the legacy “all enabled” default); only one scene besides “Global” may be enabled.", "scenes.legacyAll.fix": "Collapse to one", "scenes.preview.title": "Before entering scene “{name}” — what this will change", "scenes.preview.loading": "Working out what changes…", "scenes.preview.failed": "Preview failed: {error}", "scenes.preview.enter": "Enter the scene", "scenes.preview.dismiss": "Don't show again", "scenes.preview.dismiss.title": "Close this card and stop showing it before entering scenes (bring it back with “Turn the reminder on” at the top of this page)", "scenes.preview.toggle.off": "Turn the reminder off", "scenes.preview.toggle.on": "Turn the reminder on", "scenes.preview.toggle.title": "Show a “what will change” card before entering a scene", "scenes.preview.nochange": "Nothing to change: the runtime already matches what this scene asks for.", "scenes.preview.noArchive": "This scene has no saved profile — MCP, skills and personas are all read as “none checked”, so entering disables every one of them.", "scenes.preview.exitFirst": "Leaves “{scene}” first: what it changed is restored from the before-entering snapshot.", "scenes.preview.exitNoSnapshot": "“{scene}” is active but its before-entering snapshot is gone: leaving only clears the mode flag, the runtime will not go back.", "scenes.preview.row.serversOn": "Enables {count} MCP server(s): {names}", "scenes.preview.row.serversOff": "Disables {count} MCP server(s): {names}", "scenes.preview.row.toolsOn": "Puts back {count} MCP tool(s): {names}", "scenes.preview.row.toolsOff": "Disables {count} MCP tool(s): {names}", "scenes.preview.row.sourcesOn": "Enables {count} skill director(ies): {names}", "scenes.preview.row.sourcesOff": "Disables {count} skill director(ies): {names}", "scenes.preview.row.skillsOn": "Enables {count} skill(s): {names}", "scenes.preview.row.skillsOff": "Disables {count} skill(s): {names}", "scenes.preview.row.personasOn": "Enables {count} persona(s): {names}", "scenes.preview.row.personasOff": "Disables {count} persona(s): {names}", "scenes.preview.row.quickOn": "Enables {count} quick prompt(s): {names}", "scenes.preview.row.quickOff": "Disables {count} quick prompt(s): {names}", "scenes.preview.enter.servers": "{count} MCP server(s) stay on: {names}", "scenes.preview.enter.memories": "{count} memory(ies) enabled: {names}", "scenes.preview.row.notes": "Overrides the notes of {count} server(s) with scene notes", "scenes.field.toolTable": "Model tool-table scheme", "scenes.field.toolTable.hint": "Choose the model tool-table scheme this scene may use", "scenes.field.quickPrompts": "Quick prompts", "scenes.field.quickPrompts.current": "The enabled ones (keep as is)", "scenes.field.quickPrompts.custom": "Pick per scene", "scenes.field.quickPrompts.none": "No quick prompts yet — add some on the Prompts page", "scenes.field.quickPrompts.hint": "Sets the quick prompts for this scene", "scenes.field.quickPrompts.list": "Enabled in this scene", "scenes.form.section.basic": "Basics", "scenes.form.section.binding": "Scene bindings", "scenes.field.toolTable.none": "Not bound (keep as is)", "scenes.field.toolTable.factory": "Factory default (hides {count})", "scenes.field.toolTable.count": "hides {count}", "scenes.field.toolTable.missing": "scheme no longer exists", "scenes.preview.row.toolPreset": "Tool table switches to scheme “{name}” (hides {count})", "scenes.preview.row.toolPresetMissing": "The tool-table scheme “{name}” bound here no longer exists — nothing switches this time", "scenes.preview.more": " etc.", "scenes.preview.truncated": "(each kind lists its first 60 entries only)", "scenes.preview.stale": "{count} profile entry(ies) no longer exist in this environment and will not take effect: {items}", "scenes.doctor.title": "{count} reference(s) point at a name that no longer exists", "scenes.doctor.noneButSkipped": "The check found no dangling references", "scenes.doctor.hint": "Left behind by a rename or a delete: it still looks bound here, but nothing matches that name at runtime. Point it at the right name, or untick it in the profile.", "scenes.doctor.row": "{where}: “{name}” no longer exists", "scenes.doctor.more": "{count} more not listed", "scenes.doctor.skipped": "({domains} could not be read, so that kind was not checked)", "agm.restore.tag": "Restored after the scene", "agm.restore.tag.hint": "The global prompt from before the scene; leaving the scene restores it, so deleting it would lose that fallback", "agm.file.mismatch": "Not in AGENTS.md", "agm.file.mismatch.hint": "AGENTS.md does not currently match this preset (it may have been edited by hand); flip the switch on the left to write it back", "hist.restore.note.registered": "Restored {count} session(s); workspace “{title}” was registered again and the sessions are back in that group.", "hist.restore.note.attached": "Restored {count} session(s) and attached them to workspace “{title}”.", "hist.restore.note.skip": "Workspace ownership could not be restored for {count} session(s): {reason}", "hist.purge.title": "Delete forever?", "hist.purge.one": "Delete “{title}” and all of its records forever — this cannot be undone", "hist.purge.batch": "Delete the {count} selected sessions and all of their records forever — this cannot be undone", "hist.btn.restore": "Restore", "hist.btn.purge": "Delete forever", "hist.btn.confirmPurge": "Delete", "hist.selectGroup": "Select group {title}", "hist.group.count": "{count}", "hist.selectAll": "Select all", "hist.deselectAll": "Deselect all", "hist.btn.import": "Import sessions", "hist.btn.export": "Export sessions", "hist.selected": "{count} selected", "hist.btn.restoreSelected": "Restore selected ({count})", "hist.btn.deleteSelected": "Delete selected ({count})", "hist.stat.archived": " archived", "hist.stat.projects": " projects", "hist.stat.retention": " days kept", "hist.search.placeholder": "Search title / session id / project path", "hist.loading": "Loading…", "hist.empty.search": "No matching archived sessions", "hist.empty.none": "No archived sessions yet", "hist.import.cwd": "Project directory (optional)", "hist.import.cwd.placeholder": "Absolute path, optional (the session goes to Ungrouped)", "hist.import.drop": "Choose or drop a conversation file", "hist.import.done": "Created session {id} ({count} messages); continue it from the DSH session list.", "hist.import.lossy": "⚠ No speaker markers were found in the text, so it was folded into a single user message: the original turn structure may have been lost. If the source is a JSONL or Markdown transcript, import it with its original extension.", "hist.export.hint": "Export the selected sessions as transcript files that can be imported again", "hist.export.dir": "Export directory (absolute path, created automatically)", "hist.export.browse": "Browse and choose a folder", "hist.btn.browse": "Choose", "hist.export.pickDir": "Choose export directory", "hist.export.format": "Format", "hist.export.scope": "Session scope", "hist.export.workspace": "Workspace", "hist.export.sessions": "Sessions ({count})", "hist.export.loading": "Loading sessions…", "hist.cwd.missing": "⚠ The workspace directory no longer exists: ", "hist.tag.archived": "Archived", "hist.tag.live": "Not archived", "hist.export.empty": "No matching sessions", "hist.result.exported": "Exported {count} session(s) to {dir}", "hist.result.skipped": "; skipped {count} ({ids})", "hist.result.archived": "Archived {count} session(s) to History; manage them on the History page", "hist.result.archiveFailed": "; {count} could not be archived", "hist.btn.archiveSelected": "Archive selected", "hist.btn.archiveSelected.title": "Move the selected sessions into History so retention applies to them", "hist.export.busy": "Working…", "hist.btn.exportSelected": "Export {count} session(s)", "hist.error.title": "Operation failed", "hist.error.unknown": "Unknown error",
4641
+ "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",
4572
4642
  }
4573
4643
  };
4574
4644
 
@@ -6844,6 +6914,11 @@ function enabledMemoryCount(rules) {
6844
6914
  }
6845
6915
  // 场景输入(combobox):既能从已有场景里挑,也能直接手输一个新场景名——
6846
6916
  // 宿主 rules-create 用 mkdir(recursive) 落盘,所以「自定义场景」无需先建目录。
6917
+ //
6918
+ // `onPickFolder` 是可选的第二颗按钮(0.18.5,会话导入选工作区):选项来自登记列表,但用户
6919
+ // 也可能要挑一个从没登记过的目录,那就得走目录浏览器。它渲染在 .dsm-combo-row **里面**
6920
+ // 而不是外面 —— .dsm-field 是列向 flex,放外面会掉到下拉框下方;而 .dsm-select-menu 相对
6921
+ // .dsm-select 绝对定位,另起一行就没有对齐的锚点了。两颗都是 .dsm-btn-secondary,同高。
6847
6922
  function SceneCombo(props) {
6848
6923
  var state = react.useState(false), open = state[0], setOpen = state[1], ref = react.useRef(null);
6849
6924
  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]);
@@ -6851,7 +6926,9 @@ function enabledMemoryCount(rules) {
6851
6926
  return h("div", { className: "dsm-select", ref: ref },
6852
6927
  h("div", { className: "dsm-combo-row" },
6853
6928
  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); } }),
6854
- 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)),
6929
+ 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),
6930
+ // 开目录浏览器前先收菜单:这颗按钮在 ref 里面,document 上那个 pointerdown 关闭器不会替我关。
6931
+ 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),
6855
6932
  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);
6856
6933
  }
6857
6934
  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 }); }
@@ -8098,10 +8175,10 @@ function enabledMemoryCount(rules) {
8098
8175
  // 多选集合:勾选的 sessionId。最小 React 支持函数式初值(Set 惰性创建)。
8099
8176
  var selState = React.useState(function () { return new Set() })
8100
8177
  var selected = selState[0], setSelected = selState[1]
8101
- // 导入对话弹窗:null = 关闭;{ busy, error, result, cwd } = 打开。
8178
+ // 导入对话弹窗:null = 关闭;{ busy, error, result, cwd, pickingDir } = 打开。
8102
8179
  var impState = React.useState(null)
8103
8180
  var imp = impState[0], setImp = impState[1]
8104
- // 导出对话弹窗:null = 关闭;{ busy, error, result, outDir, format, wsFilter, selectedIds } = 打开。
8181
+ // 导出对话弹窗:null = 关闭;{ busy, error, result, outDir, format, detail, wsFilter, selectedIds } = 打开。
8105
8182
  var expState = React.useState(null)
8106
8183
  var exp = expState[0], setExp = expState[1]
8107
8184
  // 分组卡片折叠状态:key → true(收起)。默认全部展开。
@@ -8270,11 +8347,11 @@ function enabledMemoryCount(rules) {
8270
8347
  reader.readAsText(file)
8271
8348
  }
8272
8349
  function openImportPicker() {
8273
- setImp({ busy: false, error: null, result: null, cwd: '' })
8350
+ setImp({ busy: false, error: null, result: null, cwd: '', pickingDir: false })
8274
8351
  }
8275
8352
  // ---------- 导出对话:弹窗内选范围/工作区 → 勾选会话 → 写入指定目录 ----------
8276
8353
  function openExportPicker() {
8277
- setExp({ busy: false, error: null, result: null, outDir: '', format: 'markdown', wsFilter: '', archFilter: 'all', items: [], loading: true, selectedIds: new Set() })
8354
+ setExp({ busy: false, error: null, result: null, outDir: '', format: 'markdown', detail: 'simple', wsFilter: '', archFilter: 'all', items: [], loading: true, selectedIds: new Set() })
8278
8355
  // 拉取全部持久化会话(含未归档/冷会话),供选择导出。
8279
8356
  apiCall('history-sessions', {}).then(function (res) {
8280
8357
  setExp(function (prev) {
@@ -8315,7 +8392,7 @@ function enabledMemoryCount(rules) {
8315
8392
  var ids = Array.from(exp.selectedIds)
8316
8393
  if (!ids.length || !String(exp.outDir || '').trim()) return
8317
8394
  setExp(Object.assign({}, exp, { busy: true, error: null, result: null }))
8318
- apiCall('history-export', { sessionIds: ids, format: exp.format, outDir: String(exp.outDir).trim() }).then(function (res) {
8395
+ apiCall('history-export', { sessionIds: ids, format: exp.format, detail: exp.detail, outDir: String(exp.outDir).trim() }).then(function (res) {
8319
8396
  if (res && res.ok) setExp(Object.assign({}, exp, { busy: false, result: res }))
8320
8397
  else setExp(Object.assign({}, exp, { busy: false, error: (res && res.error) || t('hist.err.export') }))
8321
8398
  }).catch(function () {
@@ -8357,6 +8434,11 @@ function enabledMemoryCount(rules) {
8357
8434
  { value: 'markdown', label: 'Markdown' },
8358
8435
  { value: 'jsonl', label: 'JSONL' },
8359
8436
  ]
8437
+ var EXPORT_DETAIL_OPTS = [
8438
+ { value: 'simple', label: t('hist.export.detail.simple') },
8439
+ { value: 'tools', label: t('hist.export.detail.tools') },
8440
+ { value: 'full', label: t('hist.export.detail.full') },
8441
+ ]
8360
8442
  var EXPORT_ARCH_OPTS = [
8361
8443
  { value: 'all', label: t('hist.arch.all') },
8362
8444
  { value: 'archived', label: t('hist.arch.archived') },
@@ -8375,6 +8457,24 @@ function enabledMemoryCount(rules) {
8375
8457
  })
8376
8458
  exportWsOptions.push({ value: 'ungrouped', label: t('hist.ungrouped') })
8377
8459
  }
8460
+ // 导入弹窗的工作区候选:只列**活登记**的组,值用目录路径(history-import 吃的就是路径)。
8461
+ // 「已移除」的游离组不进来 —— 它按定义就是没登记,让它冒充「选一个工作区」会静默造出
8462
+ // 本页标着「已移除」的那种状态;真要用那个目录还有「浏览」,手输也一直留着。
8463
+ var importWsOptions = []
8464
+ ;(function () {
8465
+ var seen = {}
8466
+ function pushOption(title, path) {
8467
+ var p = String(path || '')
8468
+ if (!p || seen[p]) return
8469
+ seen[p] = true
8470
+ importWsOptions.push({ value: p, label: (title ? title + ' — ' : '') + p })
8471
+ }
8472
+ ;(data.groups || []).forEach(function (g) { if (g && g.kind === 'live') pushOption(g.title, g.path) })
8473
+ if (!importWsOptions.length) {
8474
+ var ws = data.workspaces || {}
8475
+ Object.keys(ws).forEach(function (wid) { pushOption(ws[wid] && ws[wid].title, ws[wid] && ws[wid].path) })
8476
+ }
8477
+ })()
8378
8478
  var exportItems = []
8379
8479
  if (exp) {
8380
8480
  var useGroups = !!(data.groups && data.groups.length)
@@ -8505,28 +8605,49 @@ function enabledMemoryCount(rules) {
8505
8605
  extra: React.createElement('div', null,
8506
8606
  React.createElement('div', { className: 'dsm-field' },
8507
8607
  React.createElement('label', { className: 'dsm-label' }, t('hist.import.cwd')),
8508
- React.createElement('input', { className: 'dsm-control', type: 'text', placeholder: t('hist.import.cwd.placeholder'), value: imp.cwd || '', onChange: function (e) { setImp(Object.assign({}, imp, { cwd: e.target.value })) } }))),
8608
+ // 三条路并存:从已登记的工作区挑、浏览一个文件夹、直接粘路径。
8609
+ // 标签是 combo 的**兄弟**而不是祖先 —— 嵌套 label 会把点击转给清单里第一项。
8610
+ React.createElement(SceneCombo, {
8611
+ options: importWsOptions, value: imp.cwd || '',
8612
+ placeholder: t('hist.import.cwd.placeholder'), label: t('hist.import.cwd'),
8613
+ browseLabel: t('hist.import.workspace'),
8614
+ pickFolderLabel: t('hist.import.browse'), pickFolderTitle: t('hist.export.browse'),
8615
+ disabled: imp.busy === true,
8616
+ onChange: function (v) { setImp(Object.assign({}, imp, { cwd: v })) },
8617
+ onPickFolder: function () { setImp(Object.assign({}, imp, { pickingDir: true })) },
8618
+ }),
8619
+ 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,
8620
+ importWsOptions.length ? null : React.createElement('p', { className: 'dsm-help' }, t('hist.import.workspace.none')))),
8509
8621
  onClose: function () { setImp(null) },
8510
8622
  onSubmit: function (entries, files) { doImportFile(files[0]) },
8511
8623
  }) : null,
8512
- exp ? React.createElement(Modal, { key: 'exp', className: 'dsm-modal-list dsm-export-modal', list: true, title: t('hist.btn.export'), closeLabel: t('btn.close'), onClose: function () { setExp(null) } },
8513
- React.createElement('p', { className: 'dsm-help' }, t('hist.export.hint')),
8514
- React.createElement('div', { className: 'dsm-field' },
8515
- React.createElement('label', { className: 'dsm-label' }, t('hist.export.dir')),
8516
- React.createElement('div', { className: 'dsm-dir-row' },
8517
- 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 })) } }),
8518
- 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'))),
8519
- 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),
8520
- React.createElement('div', { className: 'dsm-field' },
8521
- React.createElement('label', { className: 'dsm-label' }, t('hist.export.format')),
8522
- React.createElement(SourceSelect, { options: EXPORT_FORMAT_OPTS, value: exp.format, onChange: function (v) { setExp(Object.assign({}, exp, { format: v })) } })),
8523
- React.createElement('div', { className: 'dsm-field' },
8524
- React.createElement('label', { className: 'dsm-label' }, t('hist.export.scope')),
8525
- React.createElement(SourceSelect, { options: EXPORT_ARCH_OPTS, value: exp.archFilter, onChange: function (v) { setExp(Object.assign({}, exp, { archFilter: v, selectedIds: new Set() })) } })),
8526
- React.createElement('div', { className: 'dsm-field' },
8527
- React.createElement('label', { className: 'dsm-label' }, t('hist.export.workspace')),
8528
- React.createElement(SourceSelect, { options: exportWsOptions, value: exp.wsFilter, onChange: function (v) { setExp(Object.assign({}, exp, { wsFilter: v, selectedIds: new Set() })) } })),
8529
- React.createElement('div', { className: 'dsm-field dsm-export-body' },
8624
+ // 导出弹窗(0.18.5):字段从五行压成三行(格式|导出内容 一行、会话范围|工作区
8625
+ // 一行),整块包进 .dsm-export-form 让列表吃掉剩下的高度。范围和工作是两个正交
8626
+ // 条件,不做成笛卡尔积的单个下拉 —— 合成一行只是省位置,语义没合。
8627
+ 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) } },
8628
+ React.createElement('div', { className: 'dsm-form dsm-export-form' },
8629
+ React.createElement('p', { className: 'dsm-help' }, t('hist.export.hint')),
8630
+ React.createElement('div', { className: 'dsm-field' },
8631
+ React.createElement('label', { className: 'dsm-label' }, t('hist.export.dir')),
8632
+ React.createElement('div', { className: 'dsm-dir-row' },
8633
+ 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 })) } }),
8634
+ 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'))),
8635
+ 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),
8636
+ React.createElement('div', { className: 'dsm-field-row' },
8637
+ React.createElement('div', { className: 'dsm-field' },
8638
+ React.createElement('label', { className: 'dsm-label' }, t('hist.export.format')),
8639
+ React.createElement(SourceSelect, { options: EXPORT_FORMAT_OPTS, value: exp.format, onChange: function (v) { setExp(Object.assign({}, exp, { format: v })) } })),
8640
+ React.createElement('div', { className: 'dsm-field' },
8641
+ React.createElement('label', { className: 'dsm-label' }, t('hist.export.detail')),
8642
+ React.createElement(SourceSelect, { options: EXPORT_DETAIL_OPTS, value: exp.detail, onChange: function (v) { setExp(Object.assign({}, exp, { detail: v })) } }))),
8643
+ React.createElement('div', { className: 'dsm-field-row' },
8644
+ React.createElement('div', { className: 'dsm-field' },
8645
+ React.createElement('label', { className: 'dsm-label' }, t('hist.export.scope')),
8646
+ React.createElement(SourceSelect, { options: EXPORT_ARCH_OPTS, value: exp.archFilter, onChange: function (v) { setExp(Object.assign({}, exp, { archFilter: v, selectedIds: new Set() })) } })),
8647
+ React.createElement('div', { className: 'dsm-field' },
8648
+ React.createElement('label', { className: 'dsm-label' }, t('hist.export.workspace')),
8649
+ React.createElement(SourceSelect, { options: exportWsOptions, value: exp.wsFilter, onChange: function (v) { setExp(Object.assign({}, exp, { wsFilter: v, selectedIds: new Set() })) } }))),
8650
+ React.createElement('div', { className: 'dsm-field dsm-export-body' },
8530
8651
  React.createElement('div', { className: 'dsm-label' }, t('hist.export.sessions', { count: exportItems.length })),
8531
8652
  React.createElement('div', { className: 'dsm-source dsm-pick-list' },
8532
8653
  exp.loading ? React.createElement('div', { className: 'dsm-empty' }, t('hist.export.loading'))
@@ -8556,7 +8677,8 @@ function enabledMemoryCount(rules) {
8556
8677
  React.createElement('span', { className: 'dsm-tag' + (it.archived ? '' : ' dsm-tag-on') }, it.archived ? t('hist.tag.archived') : t('hist.tag.live'))))
8557
8678
  }) : null)
8558
8679
  })
8559
- })() : React.createElement('div', { className: 'dsm-empty' }, t('hist.export.empty')))),
8680
+ })() : React.createElement('div', { className: 'dsm-empty' }, t('hist.export.empty'))))),
8681
+ // ↑ 最后那个括号收掉 .dsm-export-form:结果条与动作行留在表单外面,才能常驻不动。
8560
8682
  exp.error ? React.createElement('div', { className: 'dsm-feedback dsm-error' }, String(exp.error)) : null,
8561
8683
  exp.result ? React.createElement('div', { className: 'dsm-feedback' },
8562
8684
  exp.result.exported
@@ -9979,810 +10101,972 @@ function enabledMemoryCount(rules) {
9979
10101
  }) : null,
9980
10102
  )
9981
10103
  }
9982
- // ---------- 子智能体页:人设文件(~/.dsh/subagents/*.md)管理 ----------
9983
- /**
9984
- * 「思考强度」档位的按 (provider, model) 缓存。
9985
- *
9986
- * 为什么放在**组件外**:放进组件里每次渲染都会重建,等于没有缓存,而档位清单要问
9987
- * adapter(`llm.resolveModelInfo`,异步、可能联网)—— 编辑人设时来回切模型不该每次都问。
9988
- * TTL 5 分钟:档位是模型能力声明,短时间内不会变;换模型时才重新问。
9989
- */
9990
- var EFFORT_CACHE_TTL_MS = 5 * 60 * 1000
9991
- var effortCacheByRoute = {}
9992
- function readEffortCache(key) {
9993
- var hit = effortCacheByRoute[key]
9994
- if (!hit) return null
9995
- if (Date.now() - hit.at > EFFORT_CACHE_TTL_MS) { delete effortCacheByRoute[key]; return null }
9996
- return hit.value
9997
- }
9998
- function writeEffortCache(key, value) { effortCacheByRoute[key] = { at: Date.now(), value: value } }
9999
- function SubagentsPage() {
10000
- var state = React.useState({ loading: true, error: null, subagents: [] })
10001
- var data = state[0], setData = state[1]
10002
- var bs = React.useState(false)
10003
- var busy = bs[0], setBusy = bs[1]
10004
- var rs = React.useState(null)
10005
- var result = rs[0], setResult = rs[1]
10006
- var ms = React.useState(null)
10007
- var modal = ms[0], setModal = ms[1]
10008
- /**
10009
- * 「思考强度」的加载状态。key = 已拉取/正在拉取的 `provider\0model`(空串 = 还没选模型)。
10010
- * 与 `cands` 分开两个 state:档位是**按模型**懒加载的,而候选目录是一次性拉的,
10011
- * 混在一起会让"切换模型"误触发整份候选重拉。
10012
- */
10013
- var es = React.useState({ key: '', loading: false, error: null, efforts: [], defaultEffort: null })
10014
- var eff = es[0], setEff = es[1]
10015
- /** 换模型导致已存档位被清掉时的一次性提示(下次换模型或关弹窗即消失)。 */
10016
- var ens = React.useState(null)
10017
- var effNotice = ens[0], setEffNotice = ens[1]
10018
- // 搜索词独立一处:`data` 在 refresh 时被整对象替换,寄在它里面会被刷新清空。
10019
- // 过滤用防抖值,输入框仍绑原值(与提示词页同一写法)。
10020
- var qs = React.useState('')
10021
- var query = qs[0], setQuery = qs[1]
10022
- var dq = useDebouncedValue(query)
10023
- // 场景锁定(v0.8):任一场景锁定 = 五个域整体冻结;本页全部写控件禁用。
10024
- var anyLocked = data.anyLocked === true
10025
- // 场景内开关由档案定义:人设开关置灰(要改就去场景页的档案编辑器里改)。
10026
- var sceneName = data.activeScene || null
10027
- // 回收站:删除人设 = 移入回收站(宿主侧 subagent-delete),这里列出/恢复/永久删除。
10028
- var ts = React.useState(null)
10029
- var trash = ts[0], setTrash = ts[1]
10030
- var tb = React.useState(false)
10031
- var trashBusy = tb[0], setTrashBusy = tb[1]
10032
- var flipRef = React.useRef(null)
10033
- useFlipReorder(flipRef)
10034
- function loadTrash(keepOpen) {
10035
- setTrash(Object.assign({ loading: true, error: null, entries: [] }, keepOpen ? trash || {} : {}))
10036
- apiCall('subagent-trash-list', {}).then(function (res) {
10037
- if (res && res.ok) setTrash({ loading: false, error: null, entries: res.trash || [] })
10038
- else setTrash({ loading: false, error: (res && res.error) || t('trash.loadFailed'), entries: [] })
10039
- }).catch(function (e) { setTrash({ loading: false, error: errMsg(e), entries: [] }) })
10040
- }
10041
- function restorePersona(item) {
10042
- setTrashBusy(true)
10043
- apiCall('subagent-trash-restore', { id: item.id }).then(function (res) {
10044
- setTrashBusy(false)
10045
- if (res && res.ok) { var sw = stateSyncWarn(t, res); setResult({ ok: !sw, warning: !!sw, text: t('trash.restored', { name: item.name }) + (sw ? ' ' + sw : '') }); refresh(true); loadTrash(true) }
10046
- else setTrash(function (cur) { return Object.assign({}, cur, { error: (res && res.error) || t('mcp.msg.failed') }) })
10047
- }).catch(function (e) { setTrashBusy(false); setTrash(function (cur) { return Object.assign({}, cur, { error: errMsg(e) }) }) })
10048
- }
10049
- function purgePersona(item) {
10050
- setTrashBusy(true)
10051
- apiCall('subagent-trash-delete', { id: item.id }).then(function (res) {
10052
- setTrashBusy(false)
10053
- if (res && res.ok) { setResult({ ok: true, text: t('trash.purged', { name: item.name }) }); loadTrash(true) }
10054
- else setTrash(function (cur) { return Object.assign({}, cur, { error: (res && res.error) || t('mcp.msg.failed') }) })
10055
- }).catch(function (e) { setTrashBusy(false); setTrash(function (cur) { return Object.assign({}, cur, { error: errMsg(e) }) }) })
10056
- }
10057
- React.useEffect(function () { if (!result || result.ok !== true) return undefined; var timer = setTimeout(function () { setResult(null) }, 2600); return function () { clearTimeout(timer) } }, [result])
10058
- function refresh(silent) {
10059
- if (!silent) setData(function (prev) { return Object.assign({}, prev, { loading: true, error: null }) })
10060
- apiCall('subagent-list', {}).then(function (r) {
10061
- // `activeScene` 必须一起搬进来:这里是把 `data` 整对象替换掉(搜索词另存就是为了
10062
- // 不被冲掉),漏一个字段等于那个字段永远是初始值 —— 场景横幅因此从来没显示过
10063
- // (2026-09-19 用户报的:技能 / 子智能体 / 记忆三页都看不到"当前处于场景…")。
10064
- if (r && r.ok) setData({ loading: false, error: null, subagents: r.subagents || [], anyLocked: r.anyLocked === true, activeScene: r.activeScene || null, toolFailures: Array.isArray(r.toolFailures) ? r.toolFailures : [] })
10065
- else setData({ loading: false, error: translateError(t, r), subagents: [], toolFailures: [] })
10066
- }).catch(function (e) { setData({ loading: false, error: errMsg(e), subagents: [], toolFailures: [] }) })
10067
- }
10068
- React.useEffect(function () { refresh() }, [])
10069
- /**
10070
- * 子智能体开关(与记忆页同一套交互):停用 = 不注入目录段、subagent_manager_list/run
10071
- * 不可见;人设文件不动。开关联动全局状态,立即提交。
10072
- */
10073
- function togglePersona(p) {
10074
- var next = !(p.enabled !== false)
10075
- setBusy(true)
10076
- apiCall('subagent-toggle', { name: p.name, enabled: next }).then(function (r) {
10077
- setBusy(false)
10078
- if (r && r.ok) {
10079
- // 场景内改开关:人设改了、档案没跟上时说清楚(详见 sceneSyncWarn)。
10080
- var warn = sceneSyncWarn(t, r)
10081
- setResult(warn ? { ok: false, warning: true, text: warn } : null)
10082
- refresh(true)
10083
- } else if (r) setResult({ ok: false, text: translateError(t, r) })
10084
- }).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) })
10085
- }
10086
- /**
10087
- * 人设表单的候选数据(模型目录 / 全体预设工具并集)只在**首次展开高级选项**时拉取:
10088
- * 宿主枚举预设需要为尚未挂载的预设建立 standing mount,不该在打开弹窗时就付这个代价。
10089
- */
10090
- var cands = React.useState({ loaded: false, loading: false, error: null, models: [], tools: [], presets: [] })
10091
- var cand = cands[0], setCand = cands[1]
10092
- function loadCandidates() {
10093
- if (cand.loaded || cand.loading) return
10094
- setCand(Object.assign({}, cand, { loading: true, error: null }))
10095
- Promise.all([apiCall('model-candidates', {}), apiCall('preset-tools', {})]).then(function (rs) {
10096
- var mres = rs[0], tres = rs[1]
10097
- var failed = (!mres || mres.ok === false) && (!tres || tres.ok === false)
10098
- setCand({
10099
- loaded: true, loading: false,
10100
- error: failed ? t('subagents.adv.loadFailed') : null,
10101
- models: (mres && mres.models) || [],
10102
- tools: (tres && tres.tools) || [],
10103
- // presets 必须原样带过来:四行模式的名字、顺序、每行的工具清单全靠它,
10104
- // 早先这里只留 models/tools,于是分组标题退回裸 id(中文界面里显示英文)。
10105
- presets: (tres && tres.presets) || [],
10106
- })
10107
- }).catch(function (e) {
10108
- setCand({ loaded: true, loading: false, error: errMsg(e), models: [], tools: [], presets: [] })
10109
- })
10110
- }
10111
- /**
10112
- * 拉取某个 (provider, model) 的思考强度档位。**只有真正展开了高级选项、且选了模型**才会
10113
- * 被调到(三个调用点:展开高级选项、模型下拉变更、自定义模型输入失焦)。
10114
- *
10115
- * 两个语义要点(都来自官方源码,见 src/subagents/service.ts 的 `reasoningEffort` 注释):
10116
- * · 档位跟模型走 —— 换模型必须重拉,旧清单对新模型无效;
10117
- * · **拉取失败一律不动已存的值**:只有"成功且当前值不在清单里"才清档。否则一次网络
10118
- * 抖动就把用户设置抹了,而失败本身已经有提示。
10119
- */
10120
- function loadEfforts(provider, model) {
10121
- setEffNotice(null)
10122
- var pv = String(provider || ''), md = String(model || '')
10123
- if (!pv || !md) { setEff({ key: '', loading: false, error: null, efforts: [], defaultEffort: null }); return }
10124
- var key = pv + '\u0000' + md
10125
- var cached = readEffortCache(key)
10126
- if (cached) {
10127
- setEff({ key: key, loading: false, error: cached.ok ? null : cached.error, efforts: cached.efforts || [], defaultEffort: cached.defaultEffort || null })
10128
- if (cached.ok) dropEffortIfGone(cached.efforts || [])
10129
- return
10130
- }
10131
- setEff({ key: key, loading: true, error: null, efforts: [], defaultEffort: null })
10132
- apiCall('model-reasoning', { provider: pv, model: md }).then(function (res) {
10133
- var value = res && res.ok !== false
10134
- ? { ok: true, efforts: res.efforts || [], defaultEffort: res.defaultEffort || null }
10135
- : { ok: false, error: (res && res.error) || t('subagents.effort.unavailable'), efforts: [], defaultEffort: null }
10136
- writeEffortCache(key, value)
10137
- setEff({ key: key, loading: false, error: value.ok ? null : value.error, efforts: value.efforts, defaultEffort: value.defaultEffort })
10138
- if (value.ok) dropEffortIfGone(value.efforts)
10139
- }).catch(function (e) {
10140
- var value = { ok: false, error: errMsg(e), efforts: [], defaultEffort: null }
10141
- writeEffortCache(key, value)
10142
- setEff({ key: key, loading: false, error: value.error, efforts: [], defaultEffort: null })
10143
- })
10144
- }
10145
- /**
10146
- * 自动清档:清单里没有当前存的那一档 → 清掉并给一次性提示。
10147
- *
10148
- * 为什么必须清:官方对**不支持**的显式档位是在 provider I/O 之前直接拒(不夹紧、不别名),
10149
- * 留着它 = 下一次委派直接起不来,而错误要到那时才看得见。
10150
- * 为什么只在"拉取成功"时判断:失败时清单是空的,拿空清单当"没有这一档"会把设置误清。
10151
- */
10152
- function dropEffortIfGone(list) {
10153
- if (!modal || modal.type !== 'editor') return
10154
- var cur = String(modal.form.reasoningEffort || '')
10155
- if (!cur) return
10156
- var has = (list || []).some(function (e) { return String(e && e.id) === cur })
10157
- if (has) return
10158
- setEffNotice(t('subagents.effort.dropped', { effort: cur }))
10159
- setForm({ reasoningEffort: '' })
10160
- }
10161
- /** 当前表单的 (provider, model) 路由键 —— 用来与 `eff.key` 比,确认清单属于当前模型。 */
10162
- function currentRouteKey() {
10163
- return String((modal && modal.form.provider) || '') + '\u0000' + String((modal && modal.form.model) || '')
10164
- }
10165
- /**
10166
- * 当前模型**已就绪**的档位。未就绪一律返回空数组:切换模型后 `eff` 里还留着上一个模型的
10167
- * 清单(新请求还没回来),照它渲染会让用户选中一个不属于这个模型的档位 —— 而官方对不支持的
10168
- * 档位是直接拒,等于埋一次"委派起不来"。
10169
- */
10170
- function currentEfforts() {
10171
- if (!modal || !modal.form.model || eff.loading || eff.error) return []
10172
- return eff.key === currentRouteKey() ? (eff.efforts || []) : []
10173
- }
10174
- /** 空档(不指定)那一行的悬停说明:把 adapter 给的默认档位名字显示出来(拿不到就不显示)。 */
10175
- function defaultEffortTitle() {
10176
- var id = eff.defaultEffort
10177
- if (!id || eff.key !== currentRouteKey()) return undefined
10178
- var hit = (eff.efforts || []).filter(function (e) { return String(e && e.id) === String(id) })[0]
10179
- return hit ? String(hit.name || hit.id) : String(id)
10180
- }
10181
- /** 高级选项:默认收起;已经在用模型/工具限制的人设自动展开(否则用户看不见自己配了什么)。 */
10182
- /** 「不限制嵌套」的取值(与 src/subagents/service.ts 的 UNLIMITED_PERSONA_CATALOG_DEPTH 同值)。 */
10183
- var CATALOG_DEPTH_UNLIMITED = 99
10184
- /** 目录注入深度的选项文案:下拉与折叠摘要共用一处,免得两处说法分叉。 */
10185
- function catalogDepthLabel(n) {
10186
- if (n === 1) return t('subagents.field.catalogDepth.onlyTop')
10187
- if (n === 2) return t('subagents.field.catalogDepth.toChild')
10188
- if (n >= CATALOG_DEPTH_UNLIMITED) return t('subagents.field.catalogDepth.unlimited')
10189
- return t('subagents.field.catalogDepth.toGrand')
10190
- }
10191
- function initialAdvanced(p) {
10192
- var modes = p && p.toolsByPreset ? Object.keys(p.toolsByPreset).length : 0
10193
- // 目录注入深度不是默认值(1)时也算"配过":它决定常驻目录出现在哪些会话,
10194
- // 藏起来会让"为什么子会话看不到目录"变得无从查起。
10195
- var budget = !!(p && typeof p.catalogDepth === 'number' && p.catalogDepth !== 1)
10196
- // 思考强度同理:配过就得让人看得见 —— 它是"下一次委派会怎么跑"的一部分,
10197
- // 藏在收起的高级选项里等于用户改过又忘了自己改过。
10198
- return !!(budget || (p && (p.model || p.provider || p.reasoningEffort)) || (p && ((p.tools || []).length || (p.toolsDeny || []).length || modes)))
10199
- }
10200
- /** 编辑态里按模式分组的名单(深拷贝:取消编辑不留痕)。 */
10201
- function cloneRules(source) {
10202
- var out = {}
10203
- Object.keys(source || {}).forEach(function (id) {
10204
- var rule = source[id] || {}
10205
- out[id] = { mode: rule.mode === 'deny' ? 'deny' : 'allow', names: (rule.names || []).slice() }
10206
- })
10207
- return out
10208
- }
10209
- function openEditor(name) {
10210
- if (!name) {
10211
- setModal({ type: 'editor', mode: 'create', advanced: false, openMode: null, stoppedRules: {}, modeQuery: '', legacyTarget: '', form: { name: '', description: '', provider: '', model: '', reasoningEffort: '', catalogDepth: 1, tools: [], toolsDeny: [], toolsByPreset: {}, body: '', output: '', error: null } })
10212
- return
10213
- }
10214
- setBusy(true)
10215
- apiCall('subagent-get', { name: name }).then(function (res) {
10216
- setBusy(false)
10217
- if (res && res.ok) {
10218
- var p = res.persona || {}
10219
- var advanced = initialAdvanced(p)
10220
- setModal({ type: 'editor', mode: 'edit', originalName: String(p.name || name), advanced: advanced, openMode: null, stoppedRules: {}, modeQuery: '', legacyTarget: '', form: {
10221
- name: p.name || name, description: p.description || '', provider: p.provider || '', model: p.model || '',
10222
- // 思考强度:服务端回空串 = 这份人设没指定(不是"没有档位")。空值不落盘,见 serializePersona。
10223
- reasoningEffort: p.reasoningEffort || '',
10224
- // 服务端回的是**生效值**(没写就是默认 1),所以这里不必再兜默认。
10225
- catalogDepth: typeof p.catalogDepth === 'number' ? p.catalogDepth : 1,
10226
- tools: (p.tools || []).slice(), toolsDeny: (p.toolsDeny || []).slice(),
10227
- toolsByPreset: cloneRules(p.toolsByPreset), body: p.body || '', output: p.output || '', error: null,
10228
- } })
10229
- // 已配过限制的人设**一打开就是展开的**(initialAdvanced)→ 候选数据必须在这里也拉,
10230
- // 否则四行模式先亮"宿主没有回传预设名单",非得点两次「高级选项」才补上(用户实测)。
10231
- // 思考强度同理:展开着就要有档位清单(它按模型懒加载,所以这里显式带上 model)。
10232
- if (advanced) { loadCandidates(); loadEfforts(p.provider, p.model) }
10233
- } else setResult({ ok: false, text: translateError(t, res) })
10234
- }).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) })
10235
- }
10236
- function setForm(patch) { if (modal && modal.type === 'editor') setModal(Object.assign({}, modal, { form: Object.assign({}, modal.form, patch) })) }
10237
- /** 列表里增删一项(勾选语义:存在 = 勾上)。 */
10238
- function toggled(list, name) {
10239
- var next = (list || []).slice()
10240
- var i = next.indexOf(name)
10241
- if (i >= 0) next.splice(i, 1); else next.push(name)
10242
- return next
10243
- }
10244
- /**
10245
- * 模型下拉的当前值。
10246
- * 宿主目录里有这对 (provider, model) → 用它;只有 model 没有 provider(跨来源手工填的)
10247
- * 或目录里没有 → 走「自定义」,同时把输入框露出来,避免把用户已配好的值悄悄改掉。
10248
- */
10249
- function modelSelectValue() {
10250
- if (!modal || modal.type !== 'editor') return ''
10251
- var f = modal.form
10252
- if (!f.model) return ''
10253
- var hit = (cand.models || []).filter(function (m) { return m.provider === f.provider && m.id === f.model })[0]
10254
- return hit ? (f.provider + '\u0000' + f.model) : '__custom__'
10255
- }
10256
- /**
10257
- * 官方四个预设的中英名走插件词典(官方 preset.yml 里只有中文名,直接用会让英文界面
10258
- * 露出中文);**自建预设用它自己的名字,不翻译** —— 与官方 display 规则一致。
10259
- * 四个预设 id → 词典键的映射只认官方发布的这四个 id。
10260
- */
10261
- var SHIPPED_PRESET_IDS = { minimal: 1, standard: 1, ptc: 1, cordis: 1 }
10262
- function presetLabel(p) {
10263
- if (!p) return ''
10264
- var id = String(p.id || '')
10265
- if (SHIPPED_PRESET_IDS[id] === 1 && String(p.trust || 'system') !== 'user') return t('preset.name.' + id)
10266
- return String(p.name || id)
10267
- }
10268
- /** 该模式下可勾选的工具名;`null` = 宿主没给出该模式的清单(读不到,不能假装是空)。 */
10269
- function modeTools(id) {
10270
- var hit = (cand.presets || []).filter(function (p) { return p.id === id })[0]
10271
- return hit && Array.isArray(hit.tools) ? hit.tools : null
10272
- }
10273
- function modeRule(id) { return (modal.form.toolsByPreset || {})[id] }
10274
- function setModeRule(id, rule) {
10275
- var next = Object.assign({}, modal.form.toolsByPreset || {})
10276
- if (rule) next[id] = rule; else delete next[id]
10277
- setModal(Object.assign({}, modal, {
10278
- form: Object.assign({}, modal.form, { toolsByPreset: next }),
10279
- openMode: modal.openMode === id && !rule ? null : modal.openMode,
10280
- }))
10281
- }
10282
- /** 模式行上的一句话状态:未开启 / 白名单 N 个 / 黑名单 N 个 / 已开启但没勾(= 不限制)。 */
10283
- function modeSummary(id) {
10284
- var rule = modeRule(id)
10285
- if (!rule) return { text: t('subagents.mode.off'), cls: 'dsm-mode-off' }
10286
- var n = (rule.names || []).length
10287
- if (!n) return { text: t('subagents.mode.empty'), cls: 'dsm-mode-off' }
10288
- return {
10289
- text: t(rule.mode === 'deny' ? 'subagents.mode.denyOn' : 'subagents.mode.allowOn', { count: n }),
10290
- cls: rule.mode === 'deny' ? 'dsm-mode-deny' : 'dsm-mode-allow',
10291
- }
10292
- }
10293
- /**
10294
- * 启动某一侧名单:生效 + 就地展开。同一模式内白/黑互斥 —— 启动一侧就把模式切过去,
10295
- * **已勾选的名字保留**(只换语义),这样"改成黑名单"不用从头再勾一遍。
10296
- */
10297
- function startMode(id, mode) {
10298
- // 刚被「关闭」掉的名字记在 stoppedRules 里:重新启用时带回,免得"关一下再开"把
10299
- // 已勾好的名单清空(用户实测踩到:以为「关闭白名单」是收起设置,再启用就空了)。
10300
- var prev = modeRule(id) || (modal.stoppedRules || {})[id]
10301
- var names = prev && Array.isArray(prev.names) ? prev.names.slice() : []
10302
- setModal(Object.assign({}, modal, {
10303
- openMode: id, modeQuery: '',
10304
- form: Object.assign({}, modal.form, {
10305
- toolsByPreset: Object.assign({}, modal.form.toolsByPreset || {}, { [id]: { mode: mode, names: names } }),
10306
- }),
10307
- }))
10308
- }
10309
- function stopMode(id) {
10310
- var prev = modeRule(id)
10311
- var stopped = Object.assign({}, modal.stoppedRules || {})
10312
- if (prev) stopped[id] = { mode: prev.mode, names: (prev.names || []).slice() }
10313
- var next = Object.assign({}, modal.form.toolsByPreset || {})
10314
- delete next[id]
10315
- setModal(Object.assign({}, modal, {
10316
- stoppedRules: stopped,
10317
- openMode: modal.openMode === id ? null : modal.openMode,
10318
- form: Object.assign({}, modal.form, { toolsByPreset: next }),
10319
- }))
10320
- }
10321
- function toggleModeTool(id, name) {
10322
- var rule = modeRule(id)
10323
- if (!rule) return
10324
- setModeRule(id, { mode: rule.mode, names: toggled(rule.names || [], name) })
10325
- }
10326
- /**
10327
- * 展开后的勾选区:只列**该模式自己的**工具。这样勾出来的名字天然都属于这个预设,
10328
- * 不会出现"在标准模式勾了只属于 PTC 的工具 → 换模式跑就启动失败"。
10329
- * MCP 工具不在这里(用户裁定):子代理照旧能用当前在跑的 MCP,那份名单在运行时并入。
10330
- */
10331
- function modeEditor(p) {
10332
- var id = p.id
10333
- var rule = modeRule(id)
10334
- if (!rule) return null
10335
- var all = modeTools(id)
10336
- if (all === null || all.length === 0) {
10337
- return React.createElement('div', { className: 'dsm-mode-body' },
10338
- React.createElement('div', { className: 'dsm-pick-empty' }, p.broken ? t('subagents.mode.broken') : t('subagents.mode.noTools')))
10339
- }
10340
- var q = String(modal.modeQuery || '').trim().toLowerCase()
10341
- var list = q ? all.filter(function (n) { return n.toLowerCase().indexOf(q) >= 0 }) : all
10342
- var picked = rule.names || []
10343
- // 与其余「全选」同一口径:全勾了就只给「取消全选」(二合一,不并列)。
10344
- var allPickedAll = all.length > 0 && all.every(function (n) { return picked.indexOf(n) >= 0 })
10345
- return React.createElement('div', { className: 'dsm-mode-body' },
10346
- picked.length
10347
- ? React.createElement('div', { className: 'dsm-tools-chips' }, picked.map(function (name) {
10348
- return React.createElement('span', { key: 'c:' + name, className: 'dsm-chip' + (rule.mode === 'deny' ? ' dsm-chip-deny' : '') },
10349
- name,
10350
- React.createElement('button', { type: 'button', title: t('subagents.tools.remove'), onClick: function () { toggleModeTool(id, name) } }, '×'))
10351
- }))
10352
- : React.createElement('span', { className: 'dsm-adv-note' }, t(rule.mode === 'deny' ? 'subagents.mode.denyEmpty' : 'subagents.mode.allowEmpty')),
10353
- React.createElement('div', { className: 'dsm-combo-row' },
10354
- React.createElement('input', {
10355
- className: 'dsm-control',
10356
- value: modal.modeQuery || '',
10357
- placeholder: t('subagents.tools.filter'),
10358
- onChange: function (e) { setModal(Object.assign({}, modal, { modeQuery: e.target.value })) },
10359
- }),
10360
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary dsm-btn-bulk', disabled: busy, onClick: function () { setModeRule(id, { mode: rule.mode, names: allPickedAll ? [] : all.slice() }) } }, bulkPair(allPickedAll ? t('bulk.unselectAll') : t('bulk.selectAll'), allPickedAll ? t('bulk.selectAll') : t('bulk.unselectAll')))),
10361
- list.length
10362
- ? React.createElement('div', { className: 'dsm-tools-grid' }, list.map(function (name) {
10363
- return pickRow({ key: 'm:' + id + ':' + name, disabled: busy, checked: picked.indexOf(name) >= 0, name: name, onChange: function () { toggleModeTool(id, name) } })
10364
- }))
10365
- : React.createElement('div', { className: 'dsm-pick-empty' }, t('scenes.mem.noMatch')))
10366
- }
10367
- /**
10368
- * 工具限制的两段说明:先「哪一行生效」,再白/黑名单语义。
10369
- * 两段是同一件事的两半,紧贴成一块 —— 分开渲染时中间隔着 12px 空行(用户 2026-09-18 指出);
10370
- * 后一段按条目清单渲染(一条一行,见 helpBullets)。
10371
- */
10372
- function modeNotes() {
10373
- return React.createElement('div', null,
10374
- React.createElement('p', { className: 'dsm-help' }, t('subagents.field.modes.hint')),
10375
- helpBullets(t, 'subagents.adv.note'))
10376
- }
10377
- /**
10378
- * 一行一个 Agent 预设(roster 顺序:标准 / PTC / 极简 / 创造 / 自建预设)。
10379
- * 默认全部折叠、全部未启动;右侧两个按钮「启动 白名单」「启动 黑名单」互斥。
10380
- */
10381
- function modeRows() {
10382
- var presets = (cand.presets || []).slice()
10383
- if (!presets.length) {
10384
- return React.createElement('div', { className: 'dsm-field' },
10385
- React.createElement('span', { className: 'dsm-label' }, t('subagents.field.modes')),
10386
- React.createElement('div', { className: 'dsm-pick-empty' }, cand.loading ? t('memory.loading') : t('subagents.mode.noPresets')),
10387
- modeNotes())
10388
- }
10389
- return React.createElement('div', { className: 'dsm-field' },
10390
- React.createElement('span', { className: 'dsm-label' }, t('subagents.field.modes')),
10391
- React.createElement('div', { className: 'dsm-modes' }, presets.map(function (p) {
10392
- var id = p.id
10393
- var rule = modeRule(id)
10394
- var summary = modeSummary(id)
10395
- var startButton = function (mode, isDeny) {
10396
- var active = !!rule && rule.mode === mode
10397
- return React.createElement('button', {
10398
- key: mode,
10399
- type: 'button',
10400
- className: 'dsm-btn dsm-btn-quiet' + (active ? (mode === 'deny' ? ' dsm-mode-btn-deny' : ' dsm-mode-btn-allow') : ''),
10401
- disabled: busy,
10402
- onClick: function () { if (active) stopMode(id); else startMode(id, mode) },
10403
- }, t(active
10404
- ? (isDeny ? 'subagents.mode.stopDeny' : 'subagents.mode.stopAllow')
10405
- : (isDeny ? 'subagents.mode.startDeny' : 'subagents.mode.startAllow')))
10406
- }
10407
- var open = modal.openMode === id
10408
- return React.createElement('div', { className: 'dsm-mode-row' + (rule ? ' dsm-mode-on' : ''), key: id },
10409
- React.createElement('div', { className: 'dsm-mode-head' },
10410
- // 行首按钮 = 展开 / 收起名单(用户实测:已配好的模式只有「关闭白名单」可点,
10411
- // 点下去是把模式停掉、名单设置再也进不去 —— 查看/编辑与启用/停用必须分开)。
10412
- React.createElement('button', {
10413
- type: 'button',
10414
- className: 'dsm-mode-head-main',
10415
- disabled: !rule,
10416
- title: rule ? t('subagents.mode.toggleHint') : '',
10417
- 'aria-expanded': open ? 'true' : 'false',
10418
- onClick: function () {
10419
- setModal(Object.assign({}, modal, { openMode: open ? null : id, modeQuery: '' }))
10420
- },
10421
- },
10422
- React.createElement('span', { className: 'dsm-mode-caret', 'aria-hidden': 'true' }, rule ? (open ? '▾' : '▸') : ''),
10423
- React.createElement('span', { className: 'dsm-mode-name' }, presetLabel(p)),
10424
- p.broken ? React.createElement('span', { className: 'dsm-tag dsm-tag-off' }, t('compat.reach.broken')) : null,
10425
- React.createElement('span', { className: 'dsm-mode-sum ' + summary.cls }, summary.text)),
10426
- React.createElement('span', { className: 'dsm-mode-actions' },
10427
- startButton('allow', false),
10428
- startButton('deny', true))),
10429
- open ? modeEditor(p) : null)
10430
- })),
10431
- modeNotes())
10432
- }
10433
- /**
10434
- * 旧格式(全局 `tools:` / `toolsDeny:`)的只读小结 + 转换入口。
10435
- * 旧键对所有模式生效,运行时仍然照旧执行;**不点转换就不动文件里的旧键**。
10436
- */
10437
- function legacyNotice() {
10438
- var allow = modal.form.tools || []
10439
- var deny = modal.form.toolsDeny || []
10440
- if (!allow.length && !deny.length) return null
10441
- var presets = cand.presets || []
10442
- var target = modal.legacyTarget || (presets[0] ? presets[0].id : '')
10443
- return React.createElement('div', { className: 'dsm-legacy' },
10444
- React.createElement('span', { className: 'dsm-adv-note' }, t('subagents.legacy.note', { allow: allow.length, deny: deny.length })),
10445
- React.createElement('div', { className: 'dsm-combo-row' },
10446
- React.createElement('div', { className: 'dsm-select' },
10447
- React.createElement('select', {
10448
- className: 'dsm-control',
10449
- value: target,
10450
- disabled: busy || !presets.length,
10451
- onChange: function (e) { setModal(Object.assign({}, modal, { legacyTarget: e.target.value })) },
10452
- }, presets.map(function (p) { return React.createElement('option', { key: p.id, value: p.id }, presetLabel(p)) }))),
10453
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || !target || !presets.length, onClick: function () {
10454
- var rule = { mode: allow.length ? 'allow' : 'deny', names: (allow.length ? allow : deny).slice() }
10455
- setModal(Object.assign({}, modal, {
10456
- openMode: target,
10457
- modeQuery: '',
10458
- legacyTarget: target,
10459
- form: Object.assign({}, modal.form, {
10460
- tools: [], toolsDeny: [],
10461
- toolsByPreset: Object.assign({}, modal.form.toolsByPreset || {}, { [target]: rule }),
10462
- }),
10463
- }))
10464
- } }, t('subagents.legacy.convert'))),
10465
- React.createElement('p', { className: 'dsm-help' }, t('subagents.legacy.hint')))
10466
- }
10467
- function toggleAdvanced() {
10468
- if (!modal || modal.type !== 'editor') return
10469
- var next = !modal.advanced
10470
- setModal(Object.assign({}, modal, { advanced: next }))
10471
- if (next) {
10472
- loadCandidates()
10473
- // 档位清单与候选目录**分开拉**:候选是本地目录(一次性),档位要问 adapter
10474
- // (按模型、可能联网),所以这里用表单里当前的模型去拉;没选模型就什么都不发。
10475
- loadEfforts(modal.form.provider, modal.form.model)
10476
- }
10477
- }
10478
- function submitEditor() {
10479
- if (!modal || modal.type !== 'editor') return
10480
- setBusy(true)
10481
- var op = modal.mode === 'create' ? 'subagent-create' : 'subagent-update'
10482
- // 编辑时名字可改:nextName 只在真的改过时才传(服务端按原名定位文件)。
10483
- var renamed = modal.mode === 'edit' && modal.originalName && modal.originalName !== modal.form.name
10484
- apiCall(op, {
10485
- name: renamed ? String(modal.originalName) : modal.form.name,
10486
- ...(renamed ? { nextName: String(modal.form.name || '').trim() } : {}),
10487
- description: modal.form.description,
10488
- provider: modal.form.provider, model: modal.form.model,
10489
- catalogDepth: modal.form.catalogDepth,
10490
- // 旧格式的全局名单原样回写(旧键不点转换就不动),新模式名单另存一块。
10491
- tools: modal.form.tools || [], toolsDeny: modal.form.toolsDeny || [],
10492
- toolsByPreset: modal.form.toolsByPreset || {},
10493
- body: modal.form.body,
10494
- output: modal.form.output || '',
10495
- }).then(function (res) {
10496
- setBusy(false)
10497
- if (res && res.ok) { var sw = stateSyncWarn(t, res); setModal(null); setResult({ ok: !sw, warning: !!sw, text: t('subagents.result.saved', { name: modal.form.name }) + (sw ? ' ' + sw : '') }); refresh(true) }
10498
- else setModal(Object.assign({}, modal, { form: Object.assign({}, modal.form, { error: translateError(t, res) }) }))
10499
- }).catch(function (e) { setBusy(false); setModal(Object.assign({}, modal, { form: Object.assign({}, modal.form, { error: errMsg(e) }) })) })
10500
- }
10501
- function submitDelete(name) {
10502
- setBusy(true)
10503
- apiCall('subagent-delete', { name: name }).then(function (res) {
10504
- setBusy(false); setModal(null)
10505
- if (res && res.ok) { var sw = stateSyncWarn(t, res); setResult({ ok: !sw, warning: !!sw, text: t('subagents.result.deleted', { name: name }) + (sw ? ' ' + sw : '') }); refresh(true) }
10506
- else setResult({ ok: false, text: translateError(t, res) })
10507
- }).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) })
10508
- }
10509
- // ── 导入人设(.md / .zip,多选或拖入;同名跳过并报告)──
10510
- function submitImport(files) {
10511
- if (!files || !files.length) return
10512
- setBusy(true)
10513
- apiCall('subagent-import', { files: files }).then(function (res) {
10514
- setBusy(false)
10515
- if (res && res.ok) {
10516
- setModal(null)
10517
- var names = res.imported || [], skipped = res.skipped || []
10518
- var text = names.length ? t('subagents.result.imported', { count: names.length, names: names.join('、') }) : t('import.none')
10519
- if (skipped.length) text += ' · ' + t('import.skipped', { items: skipped.map(function (s) { return s.name + '(' + s.reason + ')' }).join(';') })
10520
- var sw = stateSyncWarn(t, res)
10521
- if (sw) text += ' ' + sw
10522
- setResult({ ok: !sw, warning: !!sw, text: text }); refresh(true)
10523
- } else setResult({ ok: false, text: translateError(t, res) })
10524
- }).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) })
10525
- }
10526
- // 过滤只跟数据与(防抖后的)搜索词有关:每次渲染重扫一整份列表没有必要。
10527
- // FLIP 的 key 仍是 `p.name`(过滤只改变"列表里有哪些",不改变身份)。
10528
- var visibleSubagents = React.useMemo(function () {
10529
- return data.subagents.filter(function (p) { return matchPersonaQuery(p, dq) })
10530
- }, [data.subagents, dq])
10531
- // 「启用的排前面」(用户要求):与过滤分开记忆 —— 只跟过滤结果有关,弹窗 / 结果提示条
10532
- // 之类的状态变化不再重跑分区。FLIP 的 data-flip-on 仍来自 p.enabled,未动。
10533
- var orderedSubagents = React.useMemo(function () {
10534
- return enabledFirst(visibleSubagents, function (p) { return p.enabled !== false; })
10535
- }, [visibleSubagents])
10536
- var limitedCount = React.useMemo(function () {
10537
- return data.subagents.filter(function (p) { return (p.tools && p.tools.length) || (p.toolsDeny && p.toolsDeny.length) || (p.toolsByPreset && Object.keys(p.toolsByPreset).length) }).length
10538
- }, [data.subagents])
10539
- return React.createElement('section', { className: 'dsm-section' },
10540
- React.createElement('div', { className: 'dsm-head' },
10541
- React.createElement('div', { className: 'dsm-title-block' },
10542
- React.createElement('div', { className: 'dsm-title-row' },
10543
- React.createElement('h2', { className: 'dsm-title' }, t('subagents.title'))),
10544
- React.createElement('p', { className: 'dsm-desc' }, t('subagents.desc'))),
10545
- React.createElement('div', { className: 'dsm-actions' },
10546
- refreshButton(t, busy || data.loading, { className: 'dsm-btn dsm-btn-secondary', disabled: busy || data.loading, onClick: function () { refresh() } }),
10547
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || anyLocked, onClick: function () { openEditor(null) } }, t('subagents.new')),
10548
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || anyLocked, onClick: function () { setResult(null); setModal({ type: 'import' }) } }, t('subagents.import')),
10549
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || !data.subagents.length, onClick: function () { setResult(null); setModal({ type: 'export' }) } }, t('export.subagents')),
10550
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: function () { loadTrash(false) } }, t('trash.btn.open')))),
10551
- anyLocked === true ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'status' }, t('lock.banner')) : null,
10552
- // 锁定期间不显示场景那条长句:开关全是灰的,"改动会同步写进档案"没有落点,
10553
- // 两条并排还会互相矛盾(用户 2026-09-19)。
10554
- (anyLocked !== true && sceneName) ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'status' }, t('scene.switch.banner', { scene: sceneName })) : null,
10555
- React.createElement('div', { className: 'dsm-summary', style: { '--dsm-stat-cols': '2' } },
10556
- [['total', data.subagents.length, t('subagents.stat.total')],
10557
- ['limited', limitedCount, t('subagents.stat.limited')]].map(function (item) {
10558
- return React.createElement('div', { key: item[0], className: 'dsm-stat' },
10559
- React.createElement('strong', null, item[1]), item[2])
10560
- })),
10561
- // 搜索框:统计条之后(与 MCP / 技能 / 记忆 / 提示词页同一位置与样式)。
10562
- React.createElement('div', { className: 'dsm-filters' },
10563
- React.createElement('input', {
10564
- className: 'dsm-control dsm-search', value: query, 'aria-label': t('search'),
10565
- placeholder: t('subagents.search.placeholder'),
10566
- onChange: function (e) { setQuery(e.target.value) },
10567
- })),
10568
- React.createElement(Notice, { kind: result && result.warning ? 'warn' : result && result.ok ? 'ok' : 'err', text: result && result.text }),
10569
- // 走 Notice 而不是裸 div:令牌没过的提醒会在右侧自动多出一颗「填写令牌」按钮。
10570
- data.error ? React.createElement(Notice, { kind: 'err', text: String(data.error) }) : null,
10571
- // 注册失败的工具(`subagent_manager_list` / `_run`):模型侧只会「查无此工具」,
10572
- // 这条横幅是用户唯一能看到的地方。名字与原因来自服务端,句子在这里按当前语言拼。
10573
- (data.toolFailures || []).length ? React.createElement(Notice, {
10574
- kind: 'warn',
10575
- text: t('subagents.toolFailed', {
10576
- names: (data.toolFailures || []).map(function (f) { return f.name }).join('、'),
10577
- reason: String(((data.toolFailures || [])[0] || {}).reason || ''),
10578
- }),
10579
- }) : null,
10580
- data.loading && !data.subagents.length ? React.createElement('div', { className: 'dsm-empty' }, t('memory.loading'))
10581
- : !data.subagents.length ? React.createElement('div', { className: 'dsm-empty' }, t('subagents.empty'))
10582
- : !visibleSubagents.length ? React.createElement('div', { className: 'dsm-empty' }, t('subagents.empty.search'))
10583
- : React.createElement('div', { className: 'dsm-sources', ref: flipRef }, orderedSubagents.map(function (p) {
10584
- return React.createElement('div', { key: p.name, className: 'dsm-source', 'data-flip-key': p.name, 'data-flip-on': p.enabled !== false ? '1' : '0' },
10585
- React.createElement('div', { className: 'dsm-source-head' },
10586
- React.createElement('div', { className: 'dsm-source-head-main dsm-persona-main' },
10587
- React.createElement('div', { className: 'dsm-persona-name-row' },
10588
- React.createElement('span', { className: 'dsm-source-title', title: p.name }, p.name),
10589
- // 两个状态都给胶囊:以前只在停用时挂标签,启用的一行什么都不留,
10590
- // 用户没法一眼看出「没标签 = 启用」。
10591
- p.enabled === false
10592
- ? React.createElement(StatusTag, { tone: 'muted', text: t('subagents.disabled'), title: t('subagents.disabled.hint') })
10593
- : React.createElement(StatusTag, { tone: 'ok', text: t('subagents.enabled') })),
10594
- // 没有描述就不渲染这一行:空 div 仍占一行行高,行会变成一条无内容的空隙。
10595
- p.description ? React.createElement('div', { className: 'dsm-persona-desc', title: p.description }, p.description) : null),
10596
- React.createElement('div', { className: 'dsm-source-actions' },
10597
- React.createElement(Switch, { on: p.enabled !== false, disabled: busy || anyLocked, label: t('subagents.toggle') + ' ' + p.name, onClick: function () { togglePersona(p) } }),
10598
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || anyLocked, onClick: function () { openEditor(p.name) } }, t('memory.edit')),
10599
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy || anyLocked, onClick: function () { setModal({ type: 'delete', name: p.name }) } }, t('memory.delete')))))
10600
- })),
10601
- modal && modal.type === 'editor' ? React.createElement(Modal, { key: 'sedit', wide: true, title: modal.mode === 'create' ? t('subagents.create') : t('subagents.edit') + ' · ' + modal.form.name, closeLabel: t('btn.close'), onClose: function () { setModal(null) } },
10602
- React.createElement('div', { className: 'dsm-form' },
10603
- React.createElement('label', { className: 'dsm-field' },
10604
- React.createElement('span', { className: 'dsm-label' }, t('subagents.field.name')),
10605
- React.createElement('input', { className: 'dsm-control', value: modal.form.name || '', placeholder: 'code-review', onChange: function (e) { setForm({ name: e.target.value }) } })),
10606
- React.createElement('label', { className: 'dsm-field' },
10607
- // 描述会整段进「可委派的子智能体」目录,上限就是注入侧的截断长度(`CATALOG_DESC_MAX`);
10608
- // 存量人设的超长描述标红(服务端不校验这个长度,只有注入时截)。
10609
- React.createElement('div', { className: 'dsm-budget-meta' },
10610
- React.createElement('span', { className: 'dsm-label' }, t('subagents.field.description')),
10611
- React.createElement('span', { className: 'dsm-char-count' + (String(modal.form.description || '').length > CATALOG_DESC_MAX ? ' dsm-char-over' : '') }, String(modal.form.description || '').length + ' / ' + CATALOG_DESC_MAX)),
10612
- React.createElement('input', { className: 'dsm-control', value: modal.form.description || '', maxLength: CATALOG_DESC_MAX, placeholder: t('subagents.field.description.placeholder'), onChange: function (e) { setForm({ description: e.target.value }) } })),
10613
- React.createElement('label', { className: 'dsm-field' },
10614
- React.createElement('span', { className: 'dsm-label' }, t('subagents.field.body')),
10615
- React.createElement('textarea', { className: 'dsm-control dsm-textarea-md', value: modal.form.body || '', placeholder: t('subagents.field.body.placeholder'), onChange: function (e) { setForm({ body: e.target.value }) } })),
10616
- // 输出要求(frontmatter `output:`,一条一行):单独成节写进子代理的系统提示词。
10617
- // 与正文分开是有意的 —— 正文是"这个角色是什么"(散文),这里放"产出必须长什么样"
10618
- // (可检验的硬要求)。混在一起时散文会把硬要求稀释成风格提示(用户实测)。
10619
- React.createElement('label', { className: 'dsm-field' },
10620
- React.createElement('span', { className: 'dsm-label' }, t('subagents.field.output')),
10621
- React.createElement('textarea', { className: 'dsm-control dsm-textarea-md', value: modal.form.output || '', placeholder: t('subagents.field.output.placeholder'), onChange: function (e) { setForm({ output: e.target.value }) } }),
10622
- React.createElement('p', { className: 'dsm-help' }, t('subagents.field.output.hint'))),
10623
- // ── 高级选项(默认收起)──
10624
- // 模型 / 工具限制是「少数人才改」的字段,但一旦改错代价高(跨来源模型、工具名打错
10625
- // 会让子代理直接启动失败)。所以:收起来但**有值就自动展开**,并把候选做成选择器。
10626
- React.createElement('div', { className: 'dsm-adv' },
10627
- React.createElement('button', { type: 'button', className: 'dsm-adv-head', 'aria-expanded': modal.advanced === true, onClick: toggleAdvanced },
10628
- React.createElement('span', { className: 'dsm-adv-caret' }, modal.advanced ? '▼' : '▶'),
10629
- React.createElement('span', { className: 'dsm-adv-title' }, t('subagents.adv.title')),
10630
- modal.advanced ? null : React.createElement('span', { className: 'dsm-adv-note dsm-adv-sum' },
10631
- // 摘要固定两行:六项挤在一行会整句折行,把「高级选项」顶得七零八落。
10632
- // 第一行是"下一次委派会怎么跑",第二行是工具名单的存量。
10633
- React.createElement('span', null, t('subagents.adv.summary', {
10634
- model: modal.form.model ? (modal.form.provider ? modal.form.provider + '/' + modal.form.model : modal.form.model) : t('subagents.adv.inherit'),
10635
- depth: catalogDepthLabel(typeof modal.form.catalogDepth === 'number' ? modal.form.catalogDepth : 1),
10636
- // 思考强度收起来也要看得见:它是"下一次委派会怎么跑"的一部分。
10637
- // 没配时报「默认」而**不是**「继承」—— 官方语义里换模型会把继承来的那一档
10638
- // 删掉(`dsh-subagent/lib/index.js:482`),报「继承」是句假话。
10639
- effort: modal.form.reasoningEffort ? String(modal.form.reasoningEffort) : t('subagents.effort.none'),
10640
- })),
10641
- React.createElement('span', null, t('subagents.adv.summary2', {
10642
- modes: Object.keys(modal.form.toolsByPreset || {}).length,
10643
- allow: (modal.form.tools || []).length,
10644
- deny: (modal.form.toolsDeny || []).length,
10645
- })))),
10646
- modal.advanced ? React.createElement('div', { className: 'dsm-adv-body' },
10647
- // 模型 + 思考强度并排(`.dsm-field-row`):它们是同一件事的两半 —— 档位清单是
10648
- // **按模型**给的(adapter 的能力声明),换模型必须同时看这一格。
10649
- // 不复用 `.dsm-combo-row`:那个的语义是"一个控件 + 它的自定义输入",
10650
- // 它的 `.dsm-control{flex:1}` 会把两个控件都拉满。
10651
- React.createElement('div', { className: 'dsm-field-row' },
10652
- // 模型:宿主 LLM 目录里的 (provider, model) 对 + 自定义兜底。
10653
- React.createElement('div', { className: 'dsm-field' },
10654
- React.createElement('span', { className: 'dsm-label' }, t('subagents.field.model')),
10655
- React.createElement('div', { className: 'dsm-combo-row' },
10656
- React.createElement('div', { className: 'dsm-select' },
10657
- React.createElement('select', {
10658
- className: 'dsm-control',
10659
- value: modelSelectValue(),
10660
- disabled: busy || cand.loading,
10661
- onChange: function (e) {
10662
- var v = e.target.value
10663
- if (v === '') { setForm({ provider: '', model: '' }); loadEfforts('', '') }
10664
- else if (v !== '__custom__') {
10665
- var parts = v.split('\u0000')
10666
- setForm({ provider: parts[0], model: parts[1] })
10667
- // 档位跟模型走:换模型必须重拉(缓存命中时是同步的,不会闪)。
10668
- loadEfforts(parts[0], parts[1])
10669
- } else { setForm({ model: modal.form.model || '' }); loadEfforts(modal.form.provider || '', modal.form.model || '') }
10670
- },
10671
- },
10672
- React.createElement('option', { value: '' }, t('subagents.model.inherit')),
10673
- (cand.models || []).map(function (m) {
10674
- return React.createElement('option', { key: m.provider + '/' + m.id, value: m.provider + '\u0000' + m.id }, m.provider + ' · ' + m.name)
10675
- }),
10676
- React.createElement('option', { value: '__custom__' }, t('subagents.model.customOption')))),
10677
- modelSelectValue() === '__custom__'
10678
- ? React.createElement('input', {
10679
- className: 'dsm-control',
10680
- value: modal.form.model || '',
10681
- placeholder: t('subagents.field.model.placeholder'),
10682
- onChange: function (e) { setForm({ model: e.target.value }) },
10683
- // 手填模型 id 时**不按键触发**拉取(一次一个字符 = 一串请求),
10684
- // 失焦时再问一次;没变的话缓存会直接命中。
10685
- onBlur: function () { loadEfforts(modal.form.provider || '', modal.form.model || '') },
10686
- })
10687
- : null),
10688
- React.createElement('p', { className: 'dsm-help' }, cand.loading ? t('memory.loading') : t('subagents.field.model.hint'))),
10689
- // 思考强度:值就是 adapter 给的档位 id(不校验合法性,官方在 provider I/O 前会拒)。
10690
- React.createElement('div', { className: 'dsm-field' },
10691
- React.createElement('span', { className: 'dsm-label' }, t('subagents.field.effort')),
10692
- React.createElement('div', { className: 'dsm-select' },
10693
- React.createElement('select', {
10694
- className: 'dsm-control',
10695
- value: String(modal.form.reasoningEffort || ''),
10696
- disabled: busy || eff.loading || !modal.form.model,
10697
- onChange: function (e) { setEffNotice(null); setForm({ reasoningEffort: e.target.value }) },
10698
- },
10699
- React.createElement('option', { value: '', title: defaultEffortTitle() }, t('subagents.effort.default')),
10700
- currentEfforts().map(function (x) {
10701
- return React.createElement('option', { key: x.id, value: x.id, title: x.description || undefined }, x.name)
10702
- }))),
10703
- React.createElement('p', { className: 'dsm-help' },
10704
- !modal.form.model
10705
- ? t('subagents.effort.needModel')
10706
- : eff.loading
10707
- ? t('subagents.effort.loading')
10708
- : eff.error
10709
- ? String(eff.error)
10710
- : currentEfforts().length ? t('subagents.effort.hint') : t('subagents.effort.unavailable')),
10711
- effNotice ? React.createElement('p', { className: 'dsm-feedback dsm-warning' }, effNotice) : null)),
10712
- // provider 独立成一项:跨来源模型(如 sensenova)需要 provider+model 两个键同时给。
10713
- React.createElement('label', { className: 'dsm-field' },
10714
- React.createElement('span', { className: 'dsm-label' }, t('subagents.field.provider')),
10715
- React.createElement('input', { className: 'dsm-control', value: modal.form.provider || '', placeholder: t('subagents.field.provider.placeholder'), onChange: function (e) { setForm({ provider: e.target.value }) } }),
10716
- React.createElement('p', { className: 'dsm-help' }, t('subagents.field.provider.hint'))),
10717
- // 目录注入深度:人设目录注入到哪些会话(默认 1 = 只在顶层)。用下拉而不是数字
10718
- // 输入:只有 1/2/3 三个有意义的档,手写数字写错要到注入时才暴露。
10719
- //
10720
- // ⚠️ 它**不是**递归上限。2026-09-17 用户实测后改名(原名 maxDepth /「委派预算」):
10721
- // 官方 `dsh-tool-subagent` 默认 `maxDepth: 3`,子代理本来就能继续嵌套,本插件
10722
- // 也不再向官方传 maxDepth。这个字段只决定常驻目录出现在哪些深度的会话里。
10723
- React.createElement('label', { className: 'dsm-field' },
10724
- React.createElement('span', { className: 'dsm-label' }, t('subagents.field.catalogDepth')),
10725
- React.createElement('div', { className: 'dsm-select' },
10726
- React.createElement('select', {
10727
- className: 'dsm-control',
10728
- value: String(typeof modal.form.catalogDepth === 'number' ? modal.form.catalogDepth : 1),
10729
- disabled: busy,
10730
- onChange: function (e) { setForm({ catalogDepth: Number(e.target.value) }) },
10731
- }, [1, 2, 3, CATALOG_DEPTH_UNLIMITED].map(function (n) {
10732
- // 选项文字不带序号:前面再加一个「2 ·」是同一件事说两遍(用户 2026-09-17
10733
- // 指出)。下拉的 value 仍是数字,存的还是 catalogDepth 本身。
10734
- return React.createElement('option', { key: n, value: String(n) }, catalogDepthLabel(n))
10735
- }))),
10736
- helpBullets(t, 'subagents.field.catalogDepth.hint')),
10737
- // 工具限制:按 Agent 预设一行一个模式(默认全折叠、全部未启动,白/黑互斥)。
10738
- // 旧格式的全局名单(老文件 / 别处导入)在这里只读呈现,点「转换」才搬进某个模式。
10739
- legacyNotice(),
10740
- modeRows(),
10741
- cand.error ? React.createElement('div', { className: 'dsm-feedback dsm-warning' }, String(cand.error)) : null)
10742
- : null),
10743
- modal.form.error ? React.createElement('div', { className: 'dsm-feedback dsm-error', role: 'alert' }, String(modal.form.error)) : null),
10744
- React.createElement('div', { className: 'dsm-modal-actions' },
10745
- React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy || (modal.mode === 'create' && !String(modal.form.name || '').trim()) || !String(modal.form.body || '').trim(), onClick: submitEditor }, t('memory.btn.save')))) : null,
10746
- modal && modal.type === 'delete' ? React.createElement(Modal, { key: 'sdel2', title: t('subagents.delete.title'), closeLabel: t('btn.close'), onClose: function () { setModal(null) } },
10747
- React.createElement('p', { className: 'dsm-help' }, t('subagents.delete.desc', { name: modal.name })),
10748
- React.createElement('div', { className: 'dsm-modal-actions' },
10749
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { submitDelete(modal.name) } }, t('memory.btn.delete.confirm')))) : null,
10750
- modal && modal.type === 'import' ? React.createElement(ImportModal, { key: 'simp', t: t, title: t('subagents.import.title'), busy: busy, requirements: [t('upload.requirement.persona.1'), t('upload.requirement.persona.2'), t('upload.requirement.persona.3')], onClose: function () { setModal(null) }, onSubmit: submitImport }) : null,
10751
- modal && modal.type === 'export' ? React.createElement(ExportModal, {
10752
- key: 'export', t: t, title: t('export.subagents'),
10753
- items: data.subagents.map(function (p) { return { key: p.name, name: p.name, desc: p.description || '' } }),
10754
- busy: busy, result: result,
10755
- onClose: function () { setResult(null); setModal(null) },
10756
- onSubmit: function (names, outDir) {
10757
- setBusy(true); setResult(null);
10758
- apiCall('bundle-export', { kind: 'subagents', names: names, outDir: outDir }).then(function (res) {
10759
- setBusy(false);
10760
- if (res && res.ok) setResult({ ok: true, text: exportResultText(t, res) });
10761
- else setResult({ ok: false, text: translateError(t, res) });
10762
- }).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) });
10763
- },
10764
- }) : null,
10765
- trash ? React.createElement(TrashModal, {
10766
- t: t,
10767
- title: t('trash.title'),
10768
- groupTitle: t('trash.group.agents'),
10769
- groupSub: t('trash.section.agents.sub'),
10770
- locked: anyLocked,
10771
- entries: trash.entries,
10772
- loading: trash.loading,
10773
- error: trash.error,
10774
- busy: trashBusy,
10775
- onClose: function () { setTrash(null) },
10776
- onRestore: restorePersona,
10777
- onPurge: purgePersona,
10778
- }) : null)
10779
- }
10780
-
10781
- /**
10782
- * 记忆页的视图(按场景分桶 + 过滤 + 排序)。
10783
- *
10784
- * 提到组件外是为了能被 `useMemo` 包住:这段要遍历全部记忆并逐桶排序,而本页的
10785
- * 编辑器弹窗每敲一个字都会重渲染整页(`editor` 是页内 state)。传进来的 `q` 已防抖。
10104
+ // ---------- 子智能体页:人设文件(~/.dsh/subagents/*.md)管理 ----------
10105
+ /**
10106
+ * 「思考强度」档位的按 (provider, model) 缓存。
10107
+ *
10108
+ * 为什么放在**组件外**:放进组件里每次渲染都会重建,等于没有缓存,而档位清单要问
10109
+ * adapter(`llm.resolveModelInfo`,异步、可能联网)—— 编辑人设时来回切模型不该每次都问。
10110
+ * TTL 5 分钟:档位是模型能力声明,短时间内不会变;换模型时才重新问。
10111
+ */
10112
+ var EFFORT_CACHE_TTL_MS = 5 * 60 * 1000
10113
+ var effortCacheByRoute = {}
10114
+ function readEffortCache(key) {
10115
+ var hit = effortCacheByRoute[key]
10116
+ if (!hit) return null
10117
+ if (Date.now() - hit.at > EFFORT_CACHE_TTL_MS) { delete effortCacheByRoute[key]; return null }
10118
+ return hit.value
10119
+ }
10120
+ function writeEffortCache(key, value) { effortCacheByRoute[key] = { at: Date.now(), value: value } }
10121
+ function SubagentsPage() {
10122
+ var state = React.useState({ loading: true, error: null, subagents: [] })
10123
+ var data = state[0], setData = state[1]
10124
+ var bs = React.useState(false)
10125
+ var busy = bs[0], setBusy = bs[1]
10126
+ var rs = React.useState(null)
10127
+ var result = rs[0], setResult = rs[1]
10128
+ var ms = React.useState(null)
10129
+ var modal = ms[0], setModal = ms[1]
10130
+ /**
10131
+ * 「思考强度」的加载状态。key = 已拉取/正在拉取的 `provider\0model`(空串 = 还没选模型)。
10132
+ * 与 `cands` 分开两个 state:档位是**按模型**懒加载的,而候选目录是一次性拉的,
10133
+ * 混在一起会让"切换模型"误触发整份候选重拉。
10134
+ */
10135
+ var es = React.useState({ key: '', loading: false, error: null, efforts: [], defaultEffort: null })
10136
+ var eff = es[0], setEff = es[1]
10137
+ /** 换模型导致已存档位被清掉时的一次性提示(下次换模型或关弹窗即消失)。 */
10138
+ var ens = React.useState(null)
10139
+ var effNotice = ens[0], setEffNotice = ens[1]
10140
+ // 搜索词独立一处:`data` 在 refresh 时被整对象替换,寄在它里面会被刷新清空。
10141
+ // 过滤用防抖值,输入框仍绑原值(与提示词页同一写法)。
10142
+ var qs = React.useState('')
10143
+ var query = qs[0], setQuery = qs[1]
10144
+ var dq = useDebouncedValue(query)
10145
+ // 场景锁定(v0.8):任一场景锁定 = 五个域整体冻结;本页全部写控件禁用。
10146
+ var anyLocked = data.anyLocked === true
10147
+ // 场景内开关由档案定义:人设开关置灰(要改就去场景页的档案编辑器里改)。
10148
+ var sceneName = data.activeScene || null
10149
+ // 回收站:删除人设 = 移入回收站(宿主侧 subagent-delete),这里列出/恢复/永久删除。
10150
+ var ts = React.useState(null)
10151
+ var trash = ts[0], setTrash = ts[1]
10152
+ var tb = React.useState(false)
10153
+ var trashBusy = tb[0], setTrashBusy = tb[1]
10154
+ var flipRef = React.useRef(null)
10155
+ useFlipReorder(flipRef)
10156
+ function loadTrash(keepOpen) {
10157
+ setTrash(Object.assign({ loading: true, error: null, entries: [] }, keepOpen ? trash || {} : {}))
10158
+ apiCall('subagent-trash-list', {}).then(function (res) {
10159
+ if (res && res.ok) setTrash({ loading: false, error: null, entries: res.trash || [] })
10160
+ else setTrash({ loading: false, error: (res && res.error) || t('trash.loadFailed'), entries: [] })
10161
+ }).catch(function (e) { setTrash({ loading: false, error: errMsg(e), entries: [] }) })
10162
+ }
10163
+ function restorePersona(item) {
10164
+ setTrashBusy(true)
10165
+ apiCall('subagent-trash-restore', { id: item.id }).then(function (res) {
10166
+ setTrashBusy(false)
10167
+ if (res && res.ok) { var sw = stateSyncWarn(t, res); setResult({ ok: !sw, warning: !!sw, text: t('trash.restored', { name: item.name }) + (sw ? ' ' + sw : '') }); refresh(true); loadTrash(true) }
10168
+ else setTrash(function (cur) { return Object.assign({}, cur, { error: (res && res.error) || t('mcp.msg.failed') }) })
10169
+ }).catch(function (e) { setTrashBusy(false); setTrash(function (cur) { return Object.assign({}, cur, { error: errMsg(e) }) }) })
10170
+ }
10171
+ function purgePersona(item) {
10172
+ setTrashBusy(true)
10173
+ apiCall('subagent-trash-delete', { id: item.id }).then(function (res) {
10174
+ setTrashBusy(false)
10175
+ if (res && res.ok) { setResult({ ok: true, text: t('trash.purged', { name: item.name }) }); loadTrash(true) }
10176
+ else setTrash(function (cur) { return Object.assign({}, cur, { error: (res && res.error) || t('mcp.msg.failed') }) })
10177
+ }).catch(function (e) { setTrashBusy(false); setTrash(function (cur) { return Object.assign({}, cur, { error: errMsg(e) }) }) })
10178
+ }
10179
+ React.useEffect(function () { if (!result || result.ok !== true) return undefined; var timer = setTimeout(function () { setResult(null) }, 2600); return function () { clearTimeout(timer) } }, [result])
10180
+ function refresh(silent) {
10181
+ if (!silent) setData(function (prev) { return Object.assign({}, prev, { loading: true, error: null }) })
10182
+ apiCall('subagent-list', {}).then(function (r) {
10183
+ // `activeScene` 必须一起搬进来:这里是把 `data` 整对象替换掉(搜索词另存就是为了
10184
+ // 不被冲掉),漏一个字段等于那个字段永远是初始值 —— 场景横幅因此从来没显示过
10185
+ // (2026-09-19 用户报的:技能 / 子智能体 / 记忆三页都看不到"当前处于场景…")。
10186
+ if (r && r.ok) setData({ loading: false, error: null, subagents: r.subagents || [], anyLocked: r.anyLocked === true, activeScene: r.activeScene || null, toolFailures: Array.isArray(r.toolFailures) ? r.toolFailures : [] })
10187
+ else setData({ loading: false, error: translateError(t, r), subagents: [], toolFailures: [] })
10188
+ }).catch(function (e) { setData({ loading: false, error: errMsg(e), subagents: [], toolFailures: [] }) })
10189
+ }
10190
+ React.useEffect(function () { refresh() }, [])
10191
+ /**
10192
+ * 子智能体开关(与记忆页同一套交互):停用 = 不注入目录段、subagent_manager_list/run
10193
+ * 不可见;人设文件不动。开关联动全局状态,立即提交。
10194
+ */
10195
+ function togglePersona(p) {
10196
+ var next = !(p.enabled !== false)
10197
+ setBusy(true)
10198
+ apiCall('subagent-toggle', { name: p.name, enabled: next }).then(function (r) {
10199
+ setBusy(false)
10200
+ if (r && r.ok) {
10201
+ // 场景内改开关:人设改了、档案没跟上时说清楚(详见 sceneSyncWarn)。
10202
+ var warn = sceneSyncWarn(t, r)
10203
+ setResult(warn ? { ok: false, warning: true, text: warn } : null)
10204
+ refresh(true)
10205
+ } else if (r) setResult({ ok: false, text: translateError(t, r) })
10206
+ }).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) })
10207
+ }
10208
+ /**
10209
+ * 人设表单的候选数据(模型目录 / 全体预设工具并集)只在**首次展开高级选项**时拉取:
10210
+ * 宿主枚举预设需要为尚未挂载的预设建立 standing mount,不该在打开弹窗时就付这个代价。
10211
+ */
10212
+ var cands = React.useState({ loaded: false, loading: false, error: null, models: [], providers: [], tools: [], presets: [] })
10213
+ var cand = cands[0], setCand = cands[1]
10214
+ function loadCandidates() {
10215
+ if (cand.loaded || cand.loading) return
10216
+ setCand(Object.assign({}, cand, { loading: true, error: null }))
10217
+ Promise.all([apiCall('model-candidates', {}), apiCall('preset-tools', {})]).then(function (rs) {
10218
+ var mres = rs[0], tres = rs[1]
10219
+ var failed = (!mres || mres.ok === false) && (!tres || tres.ok === false)
10220
+ setCand({
10221
+ loaded: true, loading: false,
10222
+ error: failed ? t('subagents.adv.loadFailed') : null,
10223
+ models: (mres && mres.models) || [],
10224
+ // providers 是去重后的来源清单,**不能**由 models 去重反推:适配器不提供
10225
+ // listModels 时该来源一个模型都报不出来,而"来源有、目录里没模型"正是最需要
10226
+ // 手填模型 id 的场景 —— 反推会让这类来源在「模型来源」下拉里整个消失。
10227
+ providers: (mres && mres.providers) || [],
10228
+ tools: (tres && tres.tools) || [],
10229
+ // presets 必须原样带过来:四行模式的名字、顺序、每行的工具清单全靠它,
10230
+ // 早先这里只留 models/tools,于是分组标题退回裸 id(中文界面里显示英文)。
10231
+ presets: (tres && tres.presets) || [],
10232
+ })
10233
+ }).catch(function (e) {
10234
+ setCand({ loaded: true, loading: false, error: errMsg(e), models: [], providers: [], tools: [], presets: [] })
10235
+ })
10236
+ }
10237
+ /**
10238
+ * 拉取某个 (provider, model) 的思考强度档位。**只有真正展开了高级选项、且选了模型**才会
10239
+ * 被调到(三个调用点:展开高级选项、模型下拉变更、自定义模型输入失焦)。
10240
+ *
10241
+ * 两个语义要点(都来自官方源码,见 src/subagents/service.ts 的 `reasoningEffort` 注释):
10242
+ * · 档位跟模型走 —— 换模型必须重拉,旧清单对新模型无效;
10243
+ * · **拉取失败一律不动已存的值**:只有"成功且当前值不在清单里"才清档。否则一次网络
10244
+ * 抖动就把用户设置抹了,而失败本身已经有提示。
10245
+ */
10246
+ function loadEfforts(provider, model) {
10247
+ setEffNotice(null)
10248
+ var pv = String(provider || ''), md = String(model || '')
10249
+ if (!pv || !md) { setEff({ key: '', loading: false, error: null, efforts: [], defaultEffort: null }); return }
10250
+ var key = pv + '\u0000' + md
10251
+ var cached = readEffortCache(key)
10252
+ if (cached) {
10253
+ setEff({ key: key, loading: false, error: cached.ok ? null : cached.error, efforts: cached.efforts || [], defaultEffort: cached.defaultEffort || null })
10254
+ if (cached.ok) dropEffortIfGone(cached.efforts || [])
10255
+ return
10256
+ }
10257
+ setEff({ key: key, loading: true, error: null, efforts: [], defaultEffort: null })
10258
+ apiCall('model-reasoning', { provider: pv, model: md }).then(function (res) {
10259
+ var value = res && res.ok !== false
10260
+ ? { ok: true, efforts: res.efforts || [], defaultEffort: res.defaultEffort || null }
10261
+ : { ok: false, error: (res && res.error) || t('subagents.effort.unavailable'), efforts: [], defaultEffort: null }
10262
+ writeEffortCache(key, value)
10263
+ setEff({ key: key, loading: false, error: value.ok ? null : value.error, efforts: value.efforts, defaultEffort: value.defaultEffort })
10264
+ if (value.ok) dropEffortIfGone(value.efforts)
10265
+ }).catch(function (e) {
10266
+ var value = { ok: false, error: errMsg(e), efforts: [], defaultEffort: null }
10267
+ writeEffortCache(key, value)
10268
+ setEff({ key: key, loading: false, error: value.error, efforts: [], defaultEffort: null })
10269
+ })
10270
+ }
10271
+ /**
10272
+ * 自动清档:清单里没有当前存的那一档 → 清掉并给一次性提示。
10273
+ *
10274
+ * 为什么必须清:官方对**不支持**的显式档位是在 provider I/O 之前直接拒(不夹紧、不别名),
10275
+ * 留着它 = 下一次委派直接起不来,而错误要到那时才看得见。
10276
+ * 为什么只在"拉取成功"时判断:失败时清单是空的,拿空清单当"没有这一档"会把设置误清。
10277
+ */
10278
+ function dropEffortIfGone(list) {
10279
+ if (!modal || modal.type !== 'editor') return
10280
+ var cur = String(modal.form.reasoningEffort || '')
10281
+ if (!cur) return
10282
+ var has = (list || []).some(function (e) { return String(e && e.id) === cur })
10283
+ if (has) return
10284
+ setEffNotice(t('subagents.effort.dropped', { effort: cur }))
10285
+ setForm({ reasoningEffort: '' })
10286
+ }
10287
+ /** 当前表单的 (provider, model) 路由键 —— 用来与 `eff.key` 比,确认清单属于当前模型。 */
10288
+ function currentRouteKey() {
10289
+ return String((modal && modal.form.provider) || '') + '\u0000' + String((modal && modal.form.model) || '')
10290
+ }
10291
+ /**
10292
+ * 当前模型**已就绪**的档位。未就绪一律返回空数组:切换模型后 `eff` 里还留着上一个模型的
10293
+ * 清单(新请求还没回来),照它渲染会让用户选中一个不属于这个模型的档位 —— 而官方对不支持的
10294
+ * 档位是直接拒,等于埋一次"委派起不来"。
10295
+ */
10296
+ function currentEfforts() {
10297
+ if (!modal || !modal.form.model || eff.loading || eff.error) return []
10298
+ return eff.key === currentRouteKey() ? (eff.efforts || []) : []
10299
+ }
10300
+ /** 空档(不指定)那一行的悬停说明:把 adapter 给的默认档位名字显示出来(拿不到就不显示)。 */
10301
+ function defaultEffortTitle() {
10302
+ var id = eff.defaultEffort
10303
+ if (!id || eff.key !== currentRouteKey()) return undefined
10304
+ var hit = (eff.efforts || []).filter(function (e) { return String(e && e.id) === String(id) })[0]
10305
+ return hit ? String(hit.name || hit.id) : String(id)
10306
+ }
10307
+ /** 高级选项:默认收起;已经在用模型/工具限制的人设自动展开(否则用户看不见自己配了什么)。 */
10308
+ /** 「不限制嵌套」的取值(与 src/subagents/service.ts 的 UNLIMITED_PERSONA_CATALOG_DEPTH 同值)。 */
10309
+ var CATALOG_DEPTH_UNLIMITED = 99
10310
+ /** 目录注入深度的选项文案:下拉与折叠摘要共用一处,免得两处说法分叉。 */
10311
+ function catalogDepthLabel(n) {
10312
+ if (n === 1) return t('subagents.field.catalogDepth.onlyTop')
10313
+ if (n === 2) return t('subagents.field.catalogDepth.toChild')
10314
+ if (n >= CATALOG_DEPTH_UNLIMITED) return t('subagents.field.catalogDepth.unlimited')
10315
+ return t('subagents.field.catalogDepth.toGrand')
10316
+ }
10317
+ function initialAdvanced(p) {
10318
+ var modes = p && p.toolsByPreset ? Object.keys(p.toolsByPreset).length : 0
10319
+ // 目录注入深度不是默认值(1)时也算"配过":它决定常驻目录出现在哪些会话,
10320
+ // 藏起来会让"为什么子会话看不到目录"变得无从查起。
10321
+ var budget = !!(p && typeof p.catalogDepth === 'number' && p.catalogDepth !== 1)
10322
+ // 允许追问同理:它改变的是"下一次委派会怎么跑"(回执一个 id 而不是产出),
10323
+ // 藏在收起的高级选项里等于用户改过又忘了自己改过。
10324
+ var continuable = !!(p && p.continuable === true)
10325
+ // 思考强度同理:配过就得让人看得见 —— 它是"下一次委派会怎么跑"的一部分,
10326
+ // 藏在收起的高级选项里等于用户改过又忘了自己改过。
10327
+ return !!(budget || continuable || (p && (p.model || p.provider || p.reasoningEffort)) || (p && ((p.tools || []).length || (p.toolsDeny || []).length || modes)))
10328
+ }
10329
+ /** 编辑态里按模式分组的名单(深拷贝:取消编辑不留痕)。 */
10330
+ function cloneRules(source) {
10331
+ var out = {}
10332
+ Object.keys(source || {}).forEach(function (id) {
10333
+ var rule = source[id] || {}
10334
+ out[id] = { mode: rule.mode === 'deny' ? 'deny' : 'allow', names: (rule.names || []).slice() }
10335
+ })
10336
+ return out
10337
+ }
10338
+ function openEditor(name) {
10339
+ if (!name) {
10340
+ setModal({ type: 'editor', mode: 'create', advanced: false, openMode: null, stoppedRules: {}, modeQuery: '', legacyTarget: '', form: { name: '', description: '', provider: '', model: '', reasoningEffort: '', catalogDepth: 1, continuable: false, tools: [], toolsDeny: [], toolsByPreset: {}, body: '', output: '', error: null } })
10341
+ return
10342
+ }
10343
+ setBusy(true)
10344
+ apiCall('subagent-get', { name: name }).then(function (res) {
10345
+ setBusy(false)
10346
+ if (res && res.ok) {
10347
+ var p = res.persona || {}
10348
+ var advanced = initialAdvanced(p)
10349
+ setModal({ type: 'editor', mode: 'edit', originalName: String(p.name || name), advanced: advanced, openMode: null, stoppedRules: {}, modeQuery: '', legacyTarget: '', form: {
10350
+ name: p.name || name, description: p.description || '', provider: p.provider || '', model: p.model || '',
10351
+ // 思考强度:服务端回空串 = 这份人设没指定(不是"没有档位")。空值不落盘,见 serializePersona。
10352
+ reasoningEffort: p.reasoningEffort || '',
10353
+ // 服务端回的是**生效值**(没写就是默认 1),所以这里不必再兜默认。
10354
+ catalogDepth: typeof p.catalogDepth === 'number' ? p.catalogDepth : 1,
10355
+ // 服务端回布尔;老服务端不带这个字段时按 false(一次性)算。
10356
+ continuable: p.continuable === true,
10357
+ tools: (p.tools || []).slice(), toolsDeny: (p.toolsDeny || []).slice(),
10358
+ toolsByPreset: cloneRules(p.toolsByPreset), body: p.body || '', output: p.output || '', error: null,
10359
+ } })
10360
+ // 已配过限制的人设**一打开就是展开的**(initialAdvanced)→ 候选数据必须在这里也拉,
10361
+ // 否则四行模式先亮"宿主没有回传预设名单",非得点两次「高级选项」才补上(用户实测)。
10362
+ // 思考强度同理:展开着就要有档位清单(它按模型懒加载,所以这里显式带上 model)。
10363
+ if (advanced) { loadCandidates(); loadEfforts(p.provider, p.model) }
10364
+ } else setResult({ ok: false, text: translateError(t, res) })
10365
+ }).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) })
10366
+ }
10367
+ function setForm(patch) { if (modal && modal.type === 'editor') setModal(Object.assign({}, modal, { form: Object.assign({}, modal.form, patch) })) }
10368
+ /** 列表里增删一项(勾选语义:存在 = 勾上)。 */
10369
+ function toggled(list, name) {
10370
+ var next = (list || []).slice()
10371
+ var i = next.indexOf(name)
10372
+ if (i >= 0) next.splice(i, 1); else next.push(name)
10373
+ return next
10374
+ }
10375
+ /**
10376
+ * 模型下拉的当前值:`provider\0model` 复合键(与每个选项的 value 同构)。
10377
+ *
10378
+ * 目录里查不到这对 (来源, 模型) 时也**照原样给**,靠 `offCatalogModel()` 那条额外选项
10379
+ * 显示出来。0.19.0 之前这里会退回一个「自定义 / 目录里没有…」菜单项并把输入框露出来,
10380
+ * 2026-10-07 用户裁定删掉整条手填通道:来源与模型都只从宿主目录里选。
10381
+ */
10382
+ function modelSelectValue() {
10383
+ if (!modal || modal.type !== 'editor') return ''
10384
+ var f = modal.form
10385
+ if (!f.model) return ''
10386
+ return String(f.provider || '') + '\u0000' + String(f.model)
10387
+ }
10388
+ /**
10389
+ * 存的 (来源, 模型) **不在**宿主目录里时,返回 `modelSelectValue()` 的那个值(多列一条
10390
+ * 只读选项用);在目录里、或模型为空时返回 ''。
10391
+ *
10392
+ * 为什么必须有这一条:0.19.0 之前这两个字段是纯文本框,现存人设里就有目录外的值;
10393
+ * 下拉里找不到对应项时浏览器会显示第一项(「继承主会话」)—— **表单在说谎**,而保存时
10394
+ * 又把那个看不见的值写回去。多列一条带标记的选项,值就永远看得见。
10395
+ */
10396
+ function offCatalogModel() {
10397
+ if (!modal || modal.type !== 'editor') return ''
10398
+ var f = modal.form
10399
+ if (!f.model) return ''
10400
+ var provider = String(f.provider || '')
10401
+ var hit = (cand.models || []).filter(function (m) { return m.provider === provider && m.id === f.model })[0]
10402
+ return hit ? '' : modelSelectValue()
10403
+ }
10404
+ /** 「模型来源」下拉的当前值 —— 就是存的来源本身(目录外的靠 `offCatalogProvider()` 显示)。 */
10405
+ function providerSelectValue() {
10406
+ if (!modal || modal.type !== 'editor') return ''
10407
+ return String(modal.form.provider || '')
10408
+ }
10409
+ /** 存的来源不在宿主目录里时返回它(多列一条只读选项用);在目录里或为空返回 ''。 */
10410
+ function offCatalogProvider() {
10411
+ if (!modal || modal.type !== 'editor') return ''
10412
+ var v = String(modal.form.provider || '')
10413
+ if (!v) return ''
10414
+ var hit = (cand.providers || []).filter(function (p) { return p.id === v })[0]
10415
+ return hit ? '' : v
10416
+ }
10417
+ /**
10418
+ * 目录这次**真的读到了**吗。
10419
+ *
10420
+ * 「目录里没有」是一句断言,只有读成功才配说:读失败(`cand.error`)时清单是空的,
10421
+ * 照断言会把"没读到"说成"不存在"。读不到时那条额外选项仍然渲染,只是不带标记。
10422
+ */
10423
+ function catalogReady() {
10424
+ return !!(cand.loaded && !cand.loading && !cand.error)
10425
+ }
10426
+ /**
10427
+ * 来源下拉的选项文字:显示名与 id 不同时两个都给(`DeepSeek · deepseek`)——
10428
+ * 显示名给人看,id 是真正写进 frontmatter 的那个键,只给一个都算缺信息。
10429
+ */
10430
+ function providerLabel(p) {
10431
+ var id = String((p && p.id) || '')
10432
+ var name = String((p && p.name) || '')
10433
+ return name && name !== id ? name + ' · ' + id : id
10434
+ }
10435
+ /**
10436
+ * 模型下拉要列哪些模型:**来源选定后只列该来源的**(用户 2026-10-07)。
10437
+ * 不按来源收窄时,一份几十条的扁平列表里选错来源是常态,而错配的 (来源, 模型) 要到
10438
+ * 子代理真正启动时才由官方报错 —— 那时人已经在别的页面了。
10439
+ * 来源留空则列全部:此时挑中哪一条,就把它的来源一并填上(见 onChange)。
10440
+ */
10441
+ function modelOptions() {
10442
+ if (!modal || modal.type !== 'editor') return []
10443
+ var all = cand.models || []
10444
+ var p = modal.form.provider || ''
10445
+ if (!p) return all
10446
+ return all.filter(function (m) { return m.provider === p })
10447
+ }
10448
+ /** 模型选项文字:已按来源收窄时不再重复来源前缀(左边那一格就是来源)。 */
10449
+ function modelOptionLabel(m) {
10450
+ var narrowed = !!(modal && modal.type === 'editor' && modal.form.provider)
10451
+ return narrowed ? String((m && (m.name || m.id)) || '') : m.provider + ' · ' + m.name
10452
+ }
10453
+ /**
10454
+ * 「高级选项」收起态**第二行**的内容;返回 null = 这一行整个不出现。
10455
+ * 只有真配了工具限制才报:收起态挂一句「模式限制:0 项;旧格式:白名单 0 / 黑名单 0」
10456
+ * 是用一行废话占位置(用户 2026-10-07 指出)。
10457
+ */
10458
+ function advLimitLine() {
10459
+ if (!modal || modal.type !== 'editor') return null
10460
+ var modes = Object.keys(modal.form.toolsByPreset || {}).length
10461
+ var allow = (modal.form.tools || []).length
10462
+ var deny = (modal.form.toolsDeny || []).length
10463
+ if (!modes && !allow && !deny) return null
10464
+ return t('subagents.adv.summary2', {
10465
+ modes: modes,
10466
+ // 旧格式只在有值时报;分隔号写在词典值里(见 adv.summary 的 tail)。
10467
+ tail: (allow || deny) ? t('subagents.adv.legacy', { allow: allow, deny: deny }) : '',
10468
+ })
10469
+ }
10470
+ /**
10471
+ * 官方四个预设的中英名走插件词典(官方 preset.yml 里只有中文名,直接用会让英文界面
10472
+ * 露出中文);**自建预设用它自己的名字,不翻译** —— 与官方 display 规则一致。
10473
+ * 四个预设 id → 词典键的映射只认官方发布的这四个 id。
10474
+ */
10475
+ var SHIPPED_PRESET_IDS = { minimal: 1, standard: 1, ptc: 1, cordis: 1 }
10476
+ function presetLabel(p) {
10477
+ if (!p) return ''
10478
+ var id = String(p.id || '')
10479
+ if (SHIPPED_PRESET_IDS[id] === 1 && String(p.trust || 'system') !== 'user') return t('preset.name.' + id)
10480
+ return String(p.name || id)
10481
+ }
10482
+ /**
10483
+ * 该模式下可勾选的工具名 + 每个名字的归属组。
10484
+ * `null` = 宿主没给出该模式的清单(读不到,不能假装是空)。
10485
+ *
10486
+ * `groups` 缺项(老服务端没这个字段)时按 `other` 处理 —— 落错组只是位置不对,
10487
+ * 丢掉就是"界面上勾不到",那是静默降级。
10488
+ */
10489
+ function modeTools(id) {
10490
+ var hit = (cand.presets || []).filter(function (p) { return p.id === id })[0]
10491
+ if (!hit || !Array.isArray(hit.tools)) return null
10492
+ return { names: hit.tools, groups: hit.toolGroups || {} }
10493
+ }
10494
+ /** 三组的固定顺序:本插件 → 官方 → 其他。顺序即语义,别按字典序排。 */
10495
+ var TOOL_ORIGINS = ['plugin', 'official', 'other']
10496
+ function modeRule(id) { return (modal.form.toolsByPreset || {})[id] }
10497
+ function setModeRule(id, rule) {
10498
+ var next = Object.assign({}, modal.form.toolsByPreset || {})
10499
+ if (rule) next[id] = rule; else delete next[id]
10500
+ setModal(Object.assign({}, modal, {
10501
+ form: Object.assign({}, modal.form, { toolsByPreset: next }),
10502
+ openMode: modal.openMode === id && !rule ? null : modal.openMode,
10503
+ }))
10504
+ }
10505
+ /** 模式行上的一句话状态:未开启 / 白名单 N 个 / 黑名单 N 个 / 已开启但没勾(= 不限制)。 */
10506
+ function modeSummary(id) {
10507
+ var rule = modeRule(id)
10508
+ if (!rule) return { text: t('subagents.mode.off'), cls: 'dsm-mode-off' }
10509
+ var n = (rule.names || []).length
10510
+ if (!n) return { text: t('subagents.mode.empty'), cls: 'dsm-mode-off' }
10511
+ return {
10512
+ text: t(rule.mode === 'deny' ? 'subagents.mode.denyOn' : 'subagents.mode.allowOn', { count: n }),
10513
+ cls: rule.mode === 'deny' ? 'dsm-mode-deny' : 'dsm-mode-allow',
10514
+ }
10515
+ }
10516
+ /**
10517
+ * 启动某一侧名单:生效 + 就地展开。同一模式内白/黑互斥 —— 启动一侧就把模式切过去,
10518
+ * **已勾选的名字保留**(只换语义),这样"改成黑名单"不用从头再勾一遍。
10519
+ */
10520
+ function startMode(id, mode) {
10521
+ // 刚被「关闭」掉的名字记在 stoppedRules 里:重新启用时带回,免得"关一下再开"把
10522
+ // 已勾好的名单清空(用户实测踩到:以为「关闭白名单」是收起设置,再启用就空了)。
10523
+ var prev = modeRule(id) || (modal.stoppedRules || {})[id]
10524
+ var names = prev && Array.isArray(prev.names) ? prev.names.slice() : []
10525
+ setModal(Object.assign({}, modal, {
10526
+ openMode: id, modeQuery: '',
10527
+ form: Object.assign({}, modal.form, {
10528
+ toolsByPreset: Object.assign({}, modal.form.toolsByPreset || {}, { [id]: { mode: mode, names: names } }),
10529
+ }),
10530
+ }))
10531
+ }
10532
+ function stopMode(id) {
10533
+ var prev = modeRule(id)
10534
+ var stopped = Object.assign({}, modal.stoppedRules || {})
10535
+ if (prev) stopped[id] = { mode: prev.mode, names: (prev.names || []).slice() }
10536
+ var next = Object.assign({}, modal.form.toolsByPreset || {})
10537
+ delete next[id]
10538
+ setModal(Object.assign({}, modal, {
10539
+ stoppedRules: stopped,
10540
+ openMode: modal.openMode === id ? null : modal.openMode,
10541
+ form: Object.assign({}, modal.form, { toolsByPreset: next }),
10542
+ }))
10543
+ }
10544
+ function toggleModeTool(id, name) {
10545
+ var rule = modeRule(id)
10546
+ if (!rule) return
10547
+ setModeRule(id, { mode: rule.mode, names: toggled(rule.names || [], name) })
10548
+ }
10549
+ /**
10550
+ * 展开后的勾选区:只列**该模式自己的**工具,并按归属分三段
10551
+ * (插件工具 / 官方工具 / 其他工具)。这样勾出来的名字天然都属于这个预设,
10552
+ * 不会出现"在标准模式勾了只属于 PTC 的工具 → 换模式跑就启动失败"。
10553
+ * MCP 工具不在这里(用户裁定):子代理照旧能用当前在跑的 MCP,那份名单在运行时并入。
10554
+ *
10555
+ * 分组只是地标,**不改变勾选语义**:三段共用一个 `picked`,「全选 / 取消全选」仍跨组
10556
+ * 对整份清单生效;空组不渲染,筛选后为空的组也不渲染。地标复用 `segGroup`(左内缩与
10557
+ * `.dsm-pick` 同为 8px,天然对齐,所以不新增样式)。
10558
+ *
10559
+ * ⚠️ 分组买的是「一眼看出这是谁的东西」,不是「更快找到某个工具」—— 后者是上面那个
10560
+ * 筛选框的活,分组不减少滚动量。
10561
+ */
10562
+ function modeEditor(p) {
10563
+ var id = p.id
10564
+ var rule = modeRule(id)
10565
+ if (!rule) return null
10566
+ var view = modeTools(id)
10567
+ var all = view ? view.names : null
10568
+ if (all === null || all.length === 0) {
10569
+ return React.createElement('div', { className: 'dsm-mode-body' },
10570
+ React.createElement('div', { className: 'dsm-pick-empty' }, p.broken ? t('subagents.mode.broken') : t('subagents.mode.noTools')))
10571
+ }
10572
+ var q = String(modal.modeQuery || '').trim().toLowerCase()
10573
+ var list = q ? all.filter(function (n) { return n.toLowerCase().indexOf(q) >= 0 }) : all
10574
+ var picked = rule.names || []
10575
+ // 与其余「全选」同一口径:全勾了就只给「取消全选」(二合一,不并列)。
10576
+ var allPickedAll = all.length > 0 && all.every(function (n) { return picked.indexOf(n) >= 0 })
10577
+ // 按归属切段。认不出的归属(服务端没给这一项,或给了个没见过的值)一律落「其他」——
10578
+ // 落错组只是位置不对,丢掉就是"界面上勾不到"。
10579
+ var byOrigin = { plugin: [], official: [], other: [] }
10580
+ list.forEach(function (name) {
10581
+ var origin = view.groups[name]
10582
+ byOrigin[origin === 'plugin' || origin === 'official' ? origin : 'other'].push(name)
10583
+ })
10584
+ var grid = []
10585
+ TOOL_ORIGINS.forEach(function (origin) {
10586
+ var names = byOrigin[origin]
10587
+ if (!names.length) return
10588
+ grid.push(segGroup(t('subagents.tools.group.' + origin)))
10589
+ names.forEach(function (name) {
10590
+ grid.push(pickRow({ key: 'm:' + id + ':' + name, disabled: busy, checked: picked.indexOf(name) >= 0, name: name, onChange: function () { toggleModeTool(id, name) } }))
10591
+ })
10592
+ })
10593
+ return React.createElement('div', { className: 'dsm-mode-body' },
10594
+ picked.length
10595
+ ? React.createElement('div', { className: 'dsm-tools-chips' }, picked.map(function (name) {
10596
+ return React.createElement('span', { key: 'c:' + name, className: 'dsm-chip' + (rule.mode === 'deny' ? ' dsm-chip-deny' : '') },
10597
+ name,
10598
+ React.createElement('button', { type: 'button', title: t('subagents.tools.remove'), onClick: function () { toggleModeTool(id, name) } }, '×'))
10599
+ }))
10600
+ : React.createElement('span', { className: 'dsm-adv-note' }, t(rule.mode === 'deny' ? 'subagents.mode.denyEmpty' : 'subagents.mode.allowEmpty')),
10601
+ React.createElement('div', { className: 'dsm-combo-row' },
10602
+ React.createElement('input', {
10603
+ className: 'dsm-control',
10604
+ value: modal.modeQuery || '',
10605
+ placeholder: t('subagents.tools.filter'),
10606
+ onChange: function (e) { setModal(Object.assign({}, modal, { modeQuery: e.target.value })) },
10607
+ }),
10608
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary dsm-btn-bulk', disabled: busy, onClick: function () { setModeRule(id, { mode: rule.mode, names: allPickedAll ? [] : all.slice() }) } }, bulkPair(allPickedAll ? t('bulk.unselectAll') : t('bulk.selectAll'), allPickedAll ? t('bulk.selectAll') : t('bulk.unselectAll')))),
10609
+ list.length
10610
+ ? React.createElement('div', { className: 'dsm-tools-grid' }, grid)
10611
+ : React.createElement('div', { className: 'dsm-pick-empty' }, t('scenes.mem.noMatch')))
10612
+ }
10613
+ /**
10614
+ * 一行一个 Agent 预设(roster 顺序:标准 / PTC / 极简 / 创造 / 自建预设)。
10615
+ * 默认全部折叠、全部未启动;右侧两个按钮「启动 白名单」「启动 黑名单」互斥。
10616
+ *
10617
+ * 不再自带标题与说明(0.19.0 重排):标题由所在分组头承担(`subagents.adv.group.tools`),
10618
+ * 白/黑名单语义与「MCP 工具不在候选里」那一行移到分组头正下方。这两句原先挂在字段尾巴上,
10619
+ * 叠上另外几处提示后,展开态读起来是一堵墙(用户 2026-10-07 指出)。
10620
+ */
10621
+ function modeRows() {
10622
+ var presets = (cand.presets || []).slice()
10623
+ if (!presets.length) {
10624
+ return React.createElement('div', { className: 'dsm-pick-empty' }, cand.loading ? t('memory.loading') : t('subagents.mode.noPresets'))
10625
+ }
10626
+ return React.createElement('div', null,
10627
+ React.createElement('div', { className: 'dsm-modes' }, presets.map(function (p) {
10628
+ var id = p.id
10629
+ var rule = modeRule(id)
10630
+ var summary = modeSummary(id)
10631
+ var startButton = function (mode, isDeny) {
10632
+ var active = !!rule && rule.mode === mode
10633
+ return React.createElement('button', {
10634
+ key: mode,
10635
+ type: 'button',
10636
+ className: 'dsm-btn dsm-btn-quiet' + (active ? (mode === 'deny' ? ' dsm-mode-btn-deny' : ' dsm-mode-btn-allow') : ''),
10637
+ disabled: busy,
10638
+ onClick: function () { if (active) stopMode(id); else startMode(id, mode) },
10639
+ }, t(active
10640
+ ? (isDeny ? 'subagents.mode.stopDeny' : 'subagents.mode.stopAllow')
10641
+ : (isDeny ? 'subagents.mode.startDeny' : 'subagents.mode.startAllow')))
10642
+ }
10643
+ var open = modal.openMode === id
10644
+ return React.createElement('div', { className: 'dsm-mode-row' + (rule ? ' dsm-mode-on' : ''), key: id },
10645
+ React.createElement('div', { className: 'dsm-mode-head' },
10646
+ // 行首按钮 = 展开 / 收起名单(用户实测:已配好的模式只有「关闭白名单」可点,
10647
+ // 点下去是把模式停掉、名单设置再也进不去 —— 查看/编辑与启用/停用必须分开)。
10648
+ React.createElement('button', {
10649
+ type: 'button',
10650
+ className: 'dsm-mode-head-main',
10651
+ disabled: !rule,
10652
+ title: rule ? t('subagents.mode.toggleHint') : '',
10653
+ 'aria-expanded': open ? 'true' : 'false',
10654
+ onClick: function () {
10655
+ setModal(Object.assign({}, modal, { openMode: open ? null : id, modeQuery: '' }))
10656
+ },
10657
+ },
10658
+ React.createElement('span', { className: 'dsm-mode-caret', 'aria-hidden': 'true' }, rule ? (open ? '▾' : '▸') : ''),
10659
+ React.createElement('span', { className: 'dsm-mode-name' }, presetLabel(p)),
10660
+ p.broken ? React.createElement('span', { className: 'dsm-tag dsm-tag-off' }, t('compat.reach.broken')) : null,
10661
+ React.createElement('span', { className: 'dsm-mode-sum ' + summary.cls }, summary.text)),
10662
+ React.createElement('span', { className: 'dsm-mode-actions' },
10663
+ startButton('allow', false),
10664
+ startButton('deny', true))),
10665
+ open ? modeEditor(p) : null)
10666
+ })))
10667
+ }
10668
+ /**
10669
+ * 旧格式(全局 `tools:` / `toolsDeny:`)的只读小结 + 转换入口。
10670
+ * 旧键对所有模式生效,运行时仍然照旧执行;**不点转换就不动文件里的旧键**。
10671
+ */
10672
+ function legacyNotice() {
10673
+ var allow = modal.form.tools || []
10674
+ var deny = modal.form.toolsDeny || []
10675
+ if (!allow.length && !deny.length) return null
10676
+ var presets = cand.presets || []
10677
+ var target = modal.legacyTarget || (presets[0] ? presets[0].id : '')
10678
+ return React.createElement('div', { className: 'dsm-legacy' },
10679
+ React.createElement('span', { className: 'dsm-adv-note' }, t('subagents.legacy.note', { allow: allow.length, deny: deny.length })),
10680
+ React.createElement('div', { className: 'dsm-combo-row' },
10681
+ React.createElement('div', { className: 'dsm-select' },
10682
+ React.createElement('select', {
10683
+ className: 'dsm-control',
10684
+ value: target,
10685
+ disabled: busy || !presets.length,
10686
+ onChange: function (e) { setModal(Object.assign({}, modal, { legacyTarget: e.target.value })) },
10687
+ }, presets.map(function (p) { return React.createElement('option', { key: p.id, value: p.id }, presetLabel(p)) }))),
10688
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || !target || !presets.length, onClick: function () {
10689
+ var rule = { mode: allow.length ? 'allow' : 'deny', names: (allow.length ? allow : deny).slice() }
10690
+ setModal(Object.assign({}, modal, {
10691
+ openMode: target,
10692
+ modeQuery: '',
10693
+ legacyTarget: target,
10694
+ form: Object.assign({}, modal.form, {
10695
+ tools: [], toolsDeny: [],
10696
+ toolsByPreset: Object.assign({}, modal.form.toolsByPreset || {}, { [target]: rule }),
10697
+ }),
10698
+ }))
10699
+ } }, t('subagents.legacy.convert'))),
10700
+ React.createElement('p', { className: 'dsm-help' }, t('subagents.legacy.hint')))
10701
+ }
10702
+ function toggleAdvanced() {
10703
+ if (!modal || modal.type !== 'editor') return
10704
+ var next = !modal.advanced
10705
+ setModal(Object.assign({}, modal, { advanced: next }))
10706
+ if (next) {
10707
+ loadCandidates()
10708
+ // 档位清单与候选目录**分开拉**:候选是本地目录(一次性),档位要问 adapter
10709
+ // (按模型、可能联网),所以这里用表单里当前的模型去拉;没选模型就什么都不发。
10710
+ loadEfforts(modal.form.provider, modal.form.model)
10711
+ }
10712
+ }
10713
+ function submitEditor() {
10714
+ if (!modal || modal.type !== 'editor') return
10715
+ setBusy(true)
10716
+ var op = modal.mode === 'create' ? 'subagent-create' : 'subagent-update'
10717
+ // 编辑时名字可改:nextName 只在真的改过时才传(服务端按原名定位文件)。
10718
+ var renamed = modal.mode === 'edit' && modal.originalName && modal.originalName !== modal.form.name
10719
+ apiCall(op, {
10720
+ name: renamed ? String(modal.originalName) : modal.form.name,
10721
+ ...(renamed ? { nextName: String(modal.form.name || '').trim() } : {}),
10722
+ description: modal.form.description,
10723
+ provider: modal.form.provider, model: modal.form.model,
10724
+ catalogDepth: modal.form.catalogDepth,
10725
+ continuable: modal.form.continuable === true,
10726
+ // 旧格式的全局名单原样回写(旧键不点转换就不动),新模式名单另存一块。
10727
+ tools: modal.form.tools || [], toolsDeny: modal.form.toolsDeny || [],
10728
+ toolsByPreset: modal.form.toolsByPreset || {},
10729
+ body: modal.form.body,
10730
+ output: modal.form.output || '',
10731
+ }).then(function (res) {
10732
+ setBusy(false)
10733
+ if (res && res.ok) { var sw = stateSyncWarn(t, res); setModal(null); setResult({ ok: !sw, warning: !!sw, text: t('subagents.result.saved', { name: modal.form.name }) + (sw ? ' ' + sw : '') }); refresh(true) }
10734
+ else setModal(Object.assign({}, modal, { form: Object.assign({}, modal.form, { error: translateError(t, res) }) }))
10735
+ }).catch(function (e) { setBusy(false); setModal(Object.assign({}, modal, { form: Object.assign({}, modal.form, { error: errMsg(e) }) })) })
10736
+ }
10737
+ function submitDelete(name) {
10738
+ setBusy(true)
10739
+ apiCall('subagent-delete', { name: name }).then(function (res) {
10740
+ setBusy(false); setModal(null)
10741
+ if (res && res.ok) { var sw = stateSyncWarn(t, res); setResult({ ok: !sw, warning: !!sw, text: t('subagents.result.deleted', { name: name }) + (sw ? ' ' + sw : '') }); refresh(true) }
10742
+ else setResult({ ok: false, text: translateError(t, res) })
10743
+ }).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) })
10744
+ }
10745
+ // ── 导入人设(.md / .zip,多选或拖入;同名跳过并报告)──
10746
+ function submitImport(files) {
10747
+ if (!files || !files.length) return
10748
+ setBusy(true)
10749
+ apiCall('subagent-import', { files: files }).then(function (res) {
10750
+ setBusy(false)
10751
+ if (res && res.ok) {
10752
+ setModal(null)
10753
+ var names = res.imported || [], skipped = res.skipped || []
10754
+ var text = names.length ? t('subagents.result.imported', { count: names.length, names: names.join('、') }) : t('import.none')
10755
+ if (skipped.length) text += ' · ' + t('import.skipped', { items: skipped.map(function (s) { return s.name + '(' + s.reason + ')' }).join(';') })
10756
+ var sw = stateSyncWarn(t, res)
10757
+ if (sw) text += ' ' + sw
10758
+ setResult({ ok: !sw, warning: !!sw, text: text }); refresh(true)
10759
+ } else setResult({ ok: false, text: translateError(t, res) })
10760
+ }).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) })
10761
+ }
10762
+ // 过滤只跟数据与(防抖后的)搜索词有关:每次渲染重扫一整份列表没有必要。
10763
+ // FLIP 的 key 仍是 `p.name`(过滤只改变"列表里有哪些",不改变身份)。
10764
+ var visibleSubagents = React.useMemo(function () {
10765
+ return data.subagents.filter(function (p) { return matchPersonaQuery(p, dq) })
10766
+ }, [data.subagents, dq])
10767
+ // 「启用的排前面」(用户要求):与过滤分开记忆 —— 只跟过滤结果有关,弹窗 / 结果提示条
10768
+ // 之类的状态变化不再重跑分区。FLIP 的 data-flip-on 仍来自 p.enabled,未动。
10769
+ var orderedSubagents = React.useMemo(function () {
10770
+ return enabledFirst(visibleSubagents, function (p) { return p.enabled !== false; })
10771
+ }, [visibleSubagents])
10772
+ var limitedCount = React.useMemo(function () {
10773
+ return data.subagents.filter(function (p) { return (p.tools && p.tools.length) || (p.toolsDeny && p.toolsDeny.length) || (p.toolsByPreset && Object.keys(p.toolsByPreset).length) }).length
10774
+ }, [data.subagents])
10775
+ return React.createElement('section', { className: 'dsm-section' },
10776
+ React.createElement('div', { className: 'dsm-head' },
10777
+ React.createElement('div', { className: 'dsm-title-block' },
10778
+ React.createElement('div', { className: 'dsm-title-row' },
10779
+ React.createElement('h2', { className: 'dsm-title' }, t('subagents.title'))),
10780
+ React.createElement('p', { className: 'dsm-desc' }, t('subagents.desc'))),
10781
+ React.createElement('div', { className: 'dsm-actions' },
10782
+ refreshButton(t, busy || data.loading, { className: 'dsm-btn dsm-btn-secondary', disabled: busy || data.loading, onClick: function () { refresh() } }),
10783
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || anyLocked, onClick: function () { openEditor(null) } }, t('subagents.new')),
10784
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || anyLocked, onClick: function () { setResult(null); setModal({ type: 'import' }) } }, t('subagents.import')),
10785
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || !data.subagents.length, onClick: function () { setResult(null); setModal({ type: 'export' }) } }, t('export.subagents')),
10786
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: function () { loadTrash(false) } }, t('trash.btn.open')))),
10787
+ anyLocked === true ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'status' }, t('lock.banner')) : null,
10788
+ // 锁定期间不显示场景那条长句:开关全是灰的,"改动会同步写进档案"没有落点,
10789
+ // 两条并排还会互相矛盾(用户 2026-09-19)。
10790
+ (anyLocked !== true && sceneName) ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'status' }, t('scene.switch.banner', { scene: sceneName })) : null,
10791
+ React.createElement('div', { className: 'dsm-summary', style: { '--dsm-stat-cols': '2' } },
10792
+ [['total', data.subagents.length, t('subagents.stat.total')],
10793
+ ['limited', limitedCount, t('subagents.stat.limited')]].map(function (item) {
10794
+ return React.createElement('div', { key: item[0], className: 'dsm-stat' },
10795
+ React.createElement('strong', null, item[1]), item[2])
10796
+ })),
10797
+ // 搜索框:统计条之后(与 MCP / 技能 / 记忆 / 提示词页同一位置与样式)。
10798
+ React.createElement('div', { className: 'dsm-filters' },
10799
+ React.createElement('input', {
10800
+ className: 'dsm-control dsm-search', value: query, 'aria-label': t('search'),
10801
+ placeholder: t('subagents.search.placeholder'),
10802
+ onChange: function (e) { setQuery(e.target.value) },
10803
+ })),
10804
+ React.createElement(Notice, { kind: result && result.warning ? 'warn' : result && result.ok ? 'ok' : 'err', text: result && result.text }),
10805
+ // 走 Notice 而不是裸 div:令牌没过的提醒会在右侧自动多出一颗「填写令牌」按钮。
10806
+ data.error ? React.createElement(Notice, { kind: 'err', text: String(data.error) }) : null,
10807
+ // 注册失败的工具(`subagent_manager_list` / `_run`):模型侧只会「查无此工具」,
10808
+ // 这条横幅是用户唯一能看到的地方。名字与原因来自服务端,句子在这里按当前语言拼。
10809
+ (data.toolFailures || []).length ? React.createElement(Notice, {
10810
+ kind: 'warn',
10811
+ text: t('subagents.toolFailed', {
10812
+ names: (data.toolFailures || []).map(function (f) { return f.name }).join('、'),
10813
+ reason: String(((data.toolFailures || [])[0] || {}).reason || ''),
10814
+ }),
10815
+ }) : null,
10816
+ data.loading && !data.subagents.length ? React.createElement('div', { className: 'dsm-empty' }, t('memory.loading'))
10817
+ : !data.subagents.length ? React.createElement('div', { className: 'dsm-empty' }, t('subagents.empty'))
10818
+ : !visibleSubagents.length ? React.createElement('div', { className: 'dsm-empty' }, t('subagents.empty.search'))
10819
+ : React.createElement('div', { className: 'dsm-sources', ref: flipRef }, orderedSubagents.map(function (p) {
10820
+ return React.createElement('div', { key: p.name, className: 'dsm-source', 'data-flip-key': p.name, 'data-flip-on': p.enabled !== false ? '1' : '0' },
10821
+ React.createElement('div', { className: 'dsm-source-head' },
10822
+ React.createElement('div', { className: 'dsm-source-head-main dsm-persona-main' },
10823
+ React.createElement('div', { className: 'dsm-persona-name-row' },
10824
+ React.createElement('span', { className: 'dsm-source-title', title: p.name }, p.name),
10825
+ // 两个状态都给胶囊:以前只在停用时挂标签,启用的一行什么都不留,
10826
+ // 用户没法一眼看出「没标签 = 启用」。
10827
+ p.enabled === false
10828
+ ? React.createElement(StatusTag, { tone: 'muted', text: t('subagents.disabled'), title: t('subagents.disabled.hint') })
10829
+ : React.createElement(StatusTag, { tone: 'ok', text: t('subagents.enabled') })),
10830
+ // 没有描述就不渲染这一行:空 div 仍占一行行高,行会变成一条无内容的空隙。
10831
+ p.description ? React.createElement('div', { className: 'dsm-persona-desc', title: p.description }, p.description) : null),
10832
+ React.createElement('div', { className: 'dsm-source-actions' },
10833
+ React.createElement(Switch, { on: p.enabled !== false, disabled: busy || anyLocked, label: t('subagents.toggle') + ' ' + p.name, onClick: function () { togglePersona(p) } }),
10834
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || anyLocked, onClick: function () { openEditor(p.name) } }, t('memory.edit')),
10835
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy || anyLocked, onClick: function () { setModal({ type: 'delete', name: p.name }) } }, t('memory.delete')))))
10836
+ })),
10837
+ modal && modal.type === 'editor' ? React.createElement(Modal, { key: 'sedit', wide: true, title: modal.mode === 'create' ? t('subagents.create') : t('subagents.edit') + ' · ' + modal.form.name, closeLabel: t('btn.close'), onClose: function () { setModal(null) } },
10838
+ React.createElement('div', { className: 'dsm-form' },
10839
+ React.createElement('label', { className: 'dsm-field' },
10840
+ React.createElement('span', { className: 'dsm-label' }, t('subagents.field.name')),
10841
+ React.createElement('input', { className: 'dsm-control', value: modal.form.name || '', placeholder: 'code-review', onChange: function (e) { setForm({ name: e.target.value }) } })),
10842
+ React.createElement('label', { className: 'dsm-field' },
10843
+ // 描述会整段进「可委派的子智能体」目录,上限就是注入侧的截断长度(`CATALOG_DESC_MAX`);
10844
+ // 存量人设的超长描述标红(服务端不校验这个长度,只有注入时截)。
10845
+ React.createElement('div', { className: 'dsm-budget-meta' },
10846
+ React.createElement('span', { className: 'dsm-label' }, t('subagents.field.description')),
10847
+ React.createElement('span', { className: 'dsm-char-count' + (String(modal.form.description || '').length > CATALOG_DESC_MAX ? ' dsm-char-over' : '') }, String(modal.form.description || '').length + ' / ' + CATALOG_DESC_MAX)),
10848
+ React.createElement('input', { className: 'dsm-control', value: modal.form.description || '', maxLength: CATALOG_DESC_MAX, placeholder: t('subagents.field.description.placeholder'), onChange: function (e) { setForm({ description: e.target.value }) } })),
10849
+ React.createElement('label', { className: 'dsm-field' },
10850
+ React.createElement('span', { className: 'dsm-label' }, t('subagents.field.body')),
10851
+ React.createElement('textarea', { className: 'dsm-control dsm-textarea-md', value: modal.form.body || '', placeholder: t('subagents.field.body.placeholder'), onChange: function (e) { setForm({ body: e.target.value }) } })),
10852
+ // 输出要求(frontmatter `output:`,一条一行):单独成节写进子代理的系统提示词。
10853
+ // 与正文分开是有意的 —— 正文是"这个角色是什么"(散文),这里放"产出必须长什么样"
10854
+ // (可检验的硬要求)。混在一起时散文会把硬要求稀释成风格提示(用户实测)。
10855
+ React.createElement('label', { className: 'dsm-field' },
10856
+ React.createElement('span', { className: 'dsm-label' }, t('subagents.field.output')),
10857
+ React.createElement('textarea', { className: 'dsm-control dsm-textarea-md', value: modal.form.output || '', placeholder: t('subagents.field.output.placeholder'), onChange: function (e) { setForm({ output: e.target.value }) } }),
10858
+ React.createElement('p', { className: 'dsm-help' }, t('subagents.field.output.hint'))),
10859
+ // ── 高级选项(默认收起)──
10860
+ // 模型 / 工具限制是「少数人才改」的字段,但一旦改错代价高(跨来源模型、工具名打错
10861
+ // 会让子代理直接启动失败)。所以:收起来但**有值就自动展开**,并把候选做成选择器。
10862
+ React.createElement('div', { className: 'dsm-adv' },
10863
+ React.createElement('button', { type: 'button', className: 'dsm-adv-head', 'aria-expanded': modal.advanced === true, onClick: toggleAdvanced },
10864
+ React.createElement('span', { className: 'dsm-adv-caret' }, modal.advanced ? '▼' : '▶'),
10865
+ React.createElement('span', { className: 'dsm-adv-title' }, t('subagents.adv.title')),
10866
+ modal.advanced ? null : React.createElement('span', { className: 'dsm-adv-note dsm-adv-sum' },
10867
+ // 摘要**最多两行,且只报有值的那几项**(用户 2026-10-07 定稿):
10868
+ // 第一行是"下一次委派会怎么跑"(模型 / 思考强度 / 允许追问),
10869
+ // 第二行是工具限制的存量 —— 一项都没有时整行不渲染,不拿「0 项」占位置。
10870
+ // 目录注入不进摘要:它只决定目录出现在哪些会话,不影响委派本身。
10871
+ React.createElement('span', null, t('subagents.adv.summary', {
10872
+ model: modal.form.model ? (modal.form.provider ? modal.form.provider + '/' + modal.form.model : modal.form.model) : t('subagents.adv.inherit'),
10873
+ // 思考强度收起来也要看得见:它是"下一次委派会怎么跑"的一部分。
10874
+ // 没配时报「默认」而**不是**「继承」—— 官方语义里换模型会把继承来的那一档
10875
+ // 删掉(`dsh-subagent/lib/index.js:482`),报「继承」是句假话。
10876
+ effort: modal.form.reasoningEffort ? String(modal.form.reasoningEffort) : t('subagents.effort.none'),
10877
+ // 允许追问接在同一行尾巴上:它同样是"会怎么跑",而且是**最容易忘**的一项
10878
+ // (结果变成异步回执)。分隔号写在词典值里,英文侧才不用跟着改代码。
10879
+ tail: modal.form.continuable === true ? t('subagents.adv.continuable') : '',
10880
+ })),
10881
+ advLimitLine() ? React.createElement('span', null, advLimitLine()) : null)),
10882
+ modal.advanced ? React.createElement('div', { className: 'dsm-adv-body' },
10883
+ // 三组:模型 / 运行方式 / 工具限制(0.19.0 重排)。重排前是五个字段平铺、
10884
+ // 九处提示文字混用三种形态(单行 dsm-help / 项目符号 / 选择器副标题),
10885
+ // 展开态读起来是一堵墙(用户 2026-10-07 裁定)。现在:分组头 + 一条细线划开;
10886
+ // 静态说明一律不进表单,只留**状态类**提示(拉取中 / 未选模型 / 拉不到 / 报错)。
10887
+ React.createElement('div', { className: 'dsm-adv-group' },
10888
+ React.createElement('div', { className: 'dsm-adv-group-head' }, t('subagents.adv.group.model')),
10889
+ // 模型来源与模型并排:官方把 (provider, model) 当一对解析,跨来源(如 sensenova)
10890
+ // 必须两个键同时给。顺序按解析顺序 —— 先来源,后模型。
10891
+ // 两个字段并排用 `.dsm-field-row`;`.dsm-combo-row` 是**字段内部**的
10892
+ // "下拉 + 它旁边的东西"(这里只剩下拉,`.dsm-combo-row .dsm-select{flex:1}`
10893
+ // 让它填满整格 —— 缺这条时下拉宽度由选项文字撑出,换来源后标签变短、框跟着缩)。
10894
+ React.createElement('div', { className: 'dsm-field-row' },
10895
+ // 模型来源:去重后的来源清单,只从宿主目录里选(手填通道 2026-10-07 删除)。
10896
+ // 换来源要重拉档位 —— 档位清单是按 (provider, model) 给的。
10897
+ // 换来源**不清模型**:清掉是"悄悄改掉用户已配好的值",而留着会立刻显形 ——
10898
+ // 模型下拉找不到这对 (来源, 模型) 就多列一条带「目录里没有」标记的选项。
10899
+ React.createElement('div', { className: 'dsm-field' },
10900
+ React.createElement('span', { className: 'dsm-label' }, t('subagents.field.provider')),
10901
+ React.createElement('div', { className: 'dsm-combo-row' },
10902
+ React.createElement('div', { className: 'dsm-select' },
10903
+ React.createElement('select', {
10904
+ className: 'dsm-control',
10905
+ value: providerSelectValue(),
10906
+ disabled: busy || cand.loading,
10907
+ onChange: function (e) {
10908
+ var v = e.target.value
10909
+ setForm({ provider: v })
10910
+ loadEfforts(v, modal.form.model || '')
10911
+ },
10912
+ },
10913
+ React.createElement('option', { value: '' }, t('subagents.provider.inherit')),
10914
+ (cand.providers || []).map(function (p) {
10915
+ return React.createElement('option', { key: p.id, value: p.id }, providerLabel(p))
10916
+ }),
10917
+ // 目录外的旧值:列一条只读选项,值看得见、要改只能重选目录里的。
10918
+ offCatalogProvider()
10919
+ ? React.createElement('option', { value: offCatalogProvider() },
10920
+ catalogReady() ? t('subagents.field.offCatalog', { id: offCatalogProvider() }) : offCatalogProvider())
10921
+ : null)))),
10922
+ // 模型:宿主 LLM 目录里的 (provider, model) 对,只从目录里选。
10923
+ React.createElement('div', { className: 'dsm-field' },
10924
+ React.createElement('span', { className: 'dsm-label' }, t('subagents.field.model')),
10925
+ React.createElement('div', { className: 'dsm-combo-row' },
10926
+ React.createElement('div', { className: 'dsm-select' },
10927
+ React.createElement('select', {
10928
+ className: 'dsm-control',
10929
+ value: modelSelectValue(),
10930
+ disabled: busy || cand.loading,
10931
+ onChange: function (e) {
10932
+ var v = e.target.value
10933
+ if (v === '') { setForm({ provider: '', model: '' }); loadEfforts('', '') }
10934
+ else {
10935
+ var parts = v.split('\u0000')
10936
+ setForm({ provider: parts[0], model: parts[1] })
10937
+ // 档位跟模型走:换模型必须重拉(缓存命中时是同步的,不会闪)。
10938
+ loadEfforts(parts[0], parts[1])
10939
+ }
10940
+ },
10941
+ },
10942
+ React.createElement('option', { value: '' }, t('subagents.model.inherit')),
10943
+ modelOptions().map(function (m) {
10944
+ return React.createElement('option', { key: m.provider + '/' + m.id, value: m.provider + '\u0000' + m.id }, modelOptionLabel(m))
10945
+ }),
10946
+ // 目录外的旧值:同来源那条,列一条只读选项。
10947
+ offCatalogModel()
10948
+ ? React.createElement('option', { value: offCatalogModel() },
10949
+ catalogReady() ? t('subagents.field.offCatalog', { id: modal.form.model }) : modal.form.model)
10950
+ : null))),
10951
+ // 目录还在读时给一行状态;读完了不占行 —— 这里原来放的是"留空继承主会话"
10952
+ // 那句静态说明,已随 0.19.0 重排移出表单。
10953
+ cand.loading ? React.createElement('p', { className: 'dsm-help' }, t('memory.loading')) : null)),
10954
+ // 思考强度:值就是 adapter 给的档位 id(不校验合法性,官方在 provider I/O 前会拒)。
10955
+ // 单独占左半列(`.dsm-adv-half`),与上一行的列边界对齐。
10956
+ React.createElement('div', { className: 'dsm-field dsm-adv-half' },
10957
+ React.createElement('span', { className: 'dsm-label' }, t('subagents.field.effort')),
10958
+ React.createElement('div', { className: 'dsm-select' },
10959
+ React.createElement('select', {
10960
+ className: 'dsm-control',
10961
+ value: String(modal.form.reasoningEffort || ''),
10962
+ disabled: busy || eff.loading || !modal.form.model,
10963
+ onChange: function (e) { setEffNotice(null); setForm({ reasoningEffort: e.target.value }) },
10964
+ },
10965
+ React.createElement('option', { value: '', title: defaultEffortTitle() }, t('subagents.effort.default')),
10966
+ currentEfforts().map(function (x) {
10967
+ return React.createElement('option', { key: x.id, value: x.id, title: x.description || undefined }, x.name)
10968
+ }))),
10969
+ // 状态行只在"这一格为什么用不了 / 没有档位"的四种情况下出现;有档位可选时
10970
+ // 这一行不存在 —— 原来这里常驻的是"当前可选择多种思考强度"那句同义反复。
10971
+ !modal.form.model || eff.loading || eff.error || !currentEfforts().length
10972
+ ? React.createElement('p', { className: 'dsm-help' },
10973
+ !modal.form.model
10974
+ ? t('subagents.effort.needModel')
10975
+ : eff.loading
10976
+ ? t('subagents.effort.loading')
10977
+ : eff.error
10978
+ ? String(eff.error)
10979
+ : t('subagents.effort.unavailable'))
10980
+ : null,
10981
+ effNotice ? React.createElement('p', { className: 'dsm-feedback dsm-warning' }, effNotice) : null)),
10982
+ // ── 运行方式:这份人设"什么时候跑、跑在哪些会话里" ──
10983
+ React.createElement('div', { className: 'dsm-adv-group' },
10984
+ React.createElement('div', { className: 'dsm-adv-group-head' }, t('subagents.adv.group.runtime')),
10985
+ // 目录注入深度:人设目录注入到哪些会话(默认 1 = 只在顶层)。用下拉而不是数字
10986
+ // 输入:只有 1/2/3 三个有意义的档,手写数字写错要到注入时才暴露。
10987
+ //
10988
+ // ⚠️ 它**不是**递归上限。2026-09-17 用户实测后改名(原名 maxDepth /「委派预算」):
10989
+ // 官方 `dsh-tool-subagent` 默认 `maxDepth: 3`,子代理本来就能继续嵌套,本插件
10990
+ // 也不再向官方传 maxDepth。这个字段只决定常驻目录出现在哪些深度的会话里。
10991
+ React.createElement('label', { className: 'dsm-field dsm-adv-half' },
10992
+ React.createElement('span', { className: 'dsm-label' }, t('subagents.field.catalogDepth')),
10993
+ React.createElement('div', { className: 'dsm-select' },
10994
+ React.createElement('select', {
10995
+ className: 'dsm-control',
10996
+ value: String(typeof modal.form.catalogDepth === 'number' ? modal.form.catalogDepth : 1),
10997
+ disabled: busy,
10998
+ onChange: function (e) { setForm({ catalogDepth: Number(e.target.value) }) },
10999
+ }, [1, 2, 3, CATALOG_DEPTH_UNLIMITED].map(function (n) {
11000
+ // 选项文字不带序号:前面再加一个「2 ·」是同一件事说两遍(用户 2026-09-17
11001
+ // 指出)。下拉的 value 仍是数字,存的还是 catalogDepth 本身。
11002
+ return React.createElement('option', { key: n, value: String(n) }, catalogDepthLabel(n))
11003
+ })))),
11004
+ // 允许追问(frontmatter `continuable: true`):这条通道的**产出是异步的** ——
11005
+ // 委派只回执一个 agent id,子代理在后台跑、完成后自己把结果发回来,
11006
+ // 之后可以用宿主自带的 `send_message` 追问、`interrupt_agent` 中止。
11007
+ // 默认关:它占用宿主"可继续子代理"的名额(`maxActiveSubagents`,默认 8),
11008
+ // 且要求宿主加载了持久化与 session query 两个服务。
11009
+ // 机制与代价的完整说明记在 CHANGELOG / README,不进表单(0.19.0 重排)。
11010
+ React.createElement('label', { className: 'dsm-pick' },
11011
+ React.createElement('input', { type: 'checkbox', checked: modal.form.continuable === true, disabled: busy, onChange: function (e) { setForm({ continuable: e.target.checked === true }) } }),
11012
+ React.createElement('span', { className: 'dsm-pick-main' },
11013
+ React.createElement('span', { className: 'dsm-pick-name' }, t('subagents.field.continuable')),
11014
+ React.createElement('span', { className: 'dsm-pick-desc' }, t('subagents.field.continuable.short'))))),
11015
+ // ── 工具限制:按 Agent 预设一行一个模式(默认全折叠、全部未启动,白/黑互斥)。
11016
+ // 旧格式的全局名单(老文件 / 别处导入)在这里只读呈现,点「转换」才搬进某个模式。
11017
+ React.createElement('div', { className: 'dsm-adv-group' },
11018
+ React.createElement('div', { className: 'dsm-adv-group-head' }, t('subagents.adv.group.tools')),
11019
+ // 这是整块里唯一留下的静态说明,两条理由:它回答"这一屏会不会生效",
11020
+ // 而且藏着一条反直觉的事实(MCP 工具不在候选里)—— 删掉会让人以为能限制 MCP。
11021
+ // 白/黑名单语义那三句已移出表单(词面自解释,按钮与模式摘要里都在用)。
11022
+ React.createElement('p', { className: 'dsm-help' }, t('subagents.field.modes.hint')),
11023
+ legacyNotice(),
11024
+ modeRows()),
11025
+ cand.error ? React.createElement('div', { className: 'dsm-feedback dsm-warning' }, String(cand.error)) : null)
11026
+ : null),
11027
+ modal.form.error ? React.createElement('div', { className: 'dsm-feedback dsm-error', role: 'alert' }, String(modal.form.error)) : null),
11028
+ React.createElement('div', { className: 'dsm-modal-actions' },
11029
+ React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy || (modal.mode === 'create' && !String(modal.form.name || '').trim()) || !String(modal.form.body || '').trim(), onClick: submitEditor }, t('memory.btn.save')))) : null,
11030
+ modal && modal.type === 'delete' ? React.createElement(Modal, { key: 'sdel2', title: t('subagents.delete.title'), closeLabel: t('btn.close'), onClose: function () { setModal(null) } },
11031
+ React.createElement('p', { className: 'dsm-help' }, t('subagents.delete.desc', { name: modal.name })),
11032
+ React.createElement('div', { className: 'dsm-modal-actions' },
11033
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { submitDelete(modal.name) } }, t('memory.btn.delete.confirm')))) : null,
11034
+ modal && modal.type === 'import' ? React.createElement(ImportModal, { key: 'simp', t: t, title: t('subagents.import.title'), busy: busy, requirements: [t('upload.requirement.persona.1'), t('upload.requirement.persona.2'), t('upload.requirement.persona.3')], onClose: function () { setModal(null) }, onSubmit: submitImport }) : null,
11035
+ modal && modal.type === 'export' ? React.createElement(ExportModal, {
11036
+ key: 'export', t: t, title: t('export.subagents'),
11037
+ items: data.subagents.map(function (p) { return { key: p.name, name: p.name, desc: p.description || '' } }),
11038
+ busy: busy, result: result,
11039
+ onClose: function () { setResult(null); setModal(null) },
11040
+ onSubmit: function (names, outDir) {
11041
+ setBusy(true); setResult(null);
11042
+ apiCall('bundle-export', { kind: 'subagents', names: names, outDir: outDir }).then(function (res) {
11043
+ setBusy(false);
11044
+ if (res && res.ok) setResult({ ok: true, text: exportResultText(t, res) });
11045
+ else setResult({ ok: false, text: translateError(t, res) });
11046
+ }).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) });
11047
+ },
11048
+ }) : null,
11049
+ trash ? React.createElement(TrashModal, {
11050
+ t: t,
11051
+ title: t('trash.title'),
11052
+ groupTitle: t('trash.group.agents'),
11053
+ groupSub: t('trash.section.agents.sub'),
11054
+ locked: anyLocked,
11055
+ entries: trash.entries,
11056
+ loading: trash.loading,
11057
+ error: trash.error,
11058
+ busy: trashBusy,
11059
+ onClose: function () { setTrash(null) },
11060
+ onRestore: restorePersona,
11061
+ onPurge: purgePersona,
11062
+ }) : null)
11063
+ }
11064
+
11065
+ /**
11066
+ * 记忆页的视图(按场景分桶 + 过滤 + 排序)。
11067
+ *
11068
+ * 提到组件外是为了能被 `useMemo` 包住:这段要遍历全部记忆并逐桶排序,而本页的
11069
+ * 编辑器弹窗每敲一个字都会重渲染整页(`editor` 是页内 state)。传进来的 `q` 已防抖。
10786
11070
  */
10787
11071
  function memoryView(data, q, sceneFilter) {
10788
11072
  function matchRule(r) {