dsh-plugin-tool-management 0.19.0 → 0.19.1

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
@@ -504,7 +504,25 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
504
504
  .dsm-pick-group .dsm-pick{margin-left:16px}
505
505
  .dsm-pick-group .dsm-pick+.dsm-pick{margin-top:2px}
506
506
 
507
- /* 技能回收站:上下两块各占一半(目录 / 技能),滚动条在每块列表区里面,大弹窗自己不滚。
507
+ /* 回收站比其它列表弹窗高一档(用户 2026-10-10:「回收站高度加长」):640 → 760。
508
+ .dsm-modal-list 那个 640 是给「导出会话」那类"看一眼就走"的列表定的;回收站是"翻找 + 勾选 +
509
+ 批量删",本机实测提示词回收站 35 条、技能 7 条 —— 640 时一屏只看得见 8 条。
510
+ 760 不是新拍的数:它既是 .dsm-modal 自己的 max-height,也是 .dsm-modal-fixed(新增提示词)
511
+ 那个数,本插件"最高的普通弹窗"一直就是它。上一版试过 880,用户 2026-10-10 反馈
512
+ 「回收站高度不要超过下面一层的高度」—— 回收站是叠在宿主的设置弹窗上弹出来的,宿主那颗比
513
+ 880 矮,880 会从它上面探出去;760 稳稳落在它里面。
514
+ 视口那一项取 100vh - 112px 而不是 -48px:宿主设置弹窗自己就留了上下各约 48px 的边,
515
+ 窗口矮的时候跟着它一起缩,才不会又探出去。
516
+ 分栏那两块是「1fr 1fr」吃掉剩余高度,所以弹窗一长高,「目录」和「技能」两块跟着一起长
517
+ (用户同一条要求:「技能的回收站内部的目录和技能也能加高」)。
518
+ 写成两段类名(.dsm-modal.dsm-modal-trash)是为了**不靠书写顺序**压过 .dsm-modal-list 的
519
+ !important(与 .dsm-modal-export-lg 同一手法)。加在回收站那几颗弹窗上 —— 共享 TrashModal
520
+ (提示词 / 场景 / 子智能体 / MCP)与技能页自己那颗:.dsm-modal-list 还被四个页面的导出弹窗
521
+ 共用,动它就是它们一起变大。 */
522
+ .dsm-modal.dsm-modal-trash{height:min(760px,calc(100vh - 112px))!important;max-height:none!important}
523
+
524
+ /* 回收站分栏:上下两块各占一半(技能页是 目录 / 技能,提示词页是 提示词预设 / 快捷提示词),
525
+ 滚动条在每块列表区里面,大弹窗自己不滚。
508
526
  必须放在最后:盖过上面的 .dsm-modal-list .dsm-modal-body>*{flex:none},
509
527
  否则外层不撑满、两块就各按内容长,50/50 失效。宽度沿用 .dsm-modal-list 的 560px。 */
510
528
  .dsm-modal-trash-split .dsm-modal-body{overflow:hidden}
@@ -616,6 +634,47 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
616
634
  .dsm-snap-browse-foot{display:flex;align-items:center;gap:8px}
617
635
  .dsm-snap-browse-foot .dsm-help{min-width:0;flex:1;overflow-wrap:anywhere}
618
636
 
637
+ /* 回收站保留期 + 批量删除(0.19.1)。
638
+ 工具栏排在条目上方(Modal 的 body 在滚动,工具条跟着滚走的话,条目一多就够不着
639
+ 那一颗「永久删除 (N)」—— 而那正是条目多的时候才需要的按钮)。
640
+ 保留期与批量条**同一行**(用户 2026-10-10:「这应该是同一行,现在两行浪费空间」):
641
+ 工具栏是行向 flex,保留期那一组不参与收缩,批量条那一组吃掉剩余宽度并把两颗按钮顶到最右。
642
+ 真放不下时**整组折到下一行** —— 比把「已选 N 项」或说明挤成省略号好:那等于把信息删掉。 */
643
+ .dsm-trash-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:8px 12px;margin-bottom:12px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}
644
+ .dsm-trash-toolbar .dsm-trash-retention{flex:0 0 auto}
645
+ .dsm-trash-toolbar .dsm-trash-bulk{flex:1 1 auto;min-width:0;flex-wrap:nowrap}
646
+ /* 「已选 N 项」既要撑开(把两颗按钮顶到最右)又不能被压缩:flex:1 0 auto 正是这个意思。 */
647
+ .dsm-trash-toolbar .dsm-trash-bulk .dsm-note{flex:1 0 auto}
648
+ .dsm-trash-toolbar .dsm-trash-bulk>button{flex:0 0 auto}
649
+ /* 二次确认恒占整行:它是一句问句 + 两颗键,挤在保留期旁边既难看也读不清。 */
650
+ .dsm-trash-toolbar .dsm-trash-bulk-confirm{flex:1 1 100%;flex-wrap:wrap}
651
+ .dsm-trash-retention{display:flex;align-items:center;gap:8px;flex-wrap:nowrap}
652
+ .dsm-trash-retention .dsm-label{flex:none;font-weight:580}
653
+ /* .dsm-control 是 width:100% 的表单控件;塞进这一行要退回内容宽度,否则它会把整行撑满、
654
+ 把右边的说明挤到下一行。(这段样式整体在模板字符串里,注释里不能出现反引号。) */
655
+ .dsm-trash-retention .dsm-control{width:auto;min-width:112px;flex:none}
656
+ /* 保留期这一行**恒为一行**(用户 2026-10-10:「保留期一行就够了」)。
657
+ 所以说明必须短(trash.retention.hint = 「超期自动删除」),全文走 title 悬停;
658
+ nowrap + 省略号只是兜底:万一窄到放不下,宁可截掉尾部也不折行把下拉框顶上去。 */
659
+ .dsm-trash-hint{min-width:0;flex:0 1 auto;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
660
+ /* 出错时反过来:允许换行、不截断 —— 失败原因截成半句等于没说。 */
661
+ .dsm-trash-retention-err{flex:1 1 auto;white-space:normal;overflow-wrap:anywhere;color:var(--dsw-alias-state-error-primary)}
662
+ .dsm-trash-bulk{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
663
+ /* 「已选 N 项」占满左侧剩余空间 → 把「全选」与唯一的「永久删除」一起顶到最右。 */
664
+ .dsm-trash-bulk .dsm-note{min-width:0;flex:1}
665
+ .dsm-trash-bulk-confirm{padding:6px 8px;border-radius:8px;background:var(--dsw-alias-interactive-bg-hover)}
666
+ .dsm-trash-bulk-confirm .dsm-note{font-weight:570;color:var(--dsw-alias-state-error-primary);white-space:normal}
667
+ /* 技能回收站:两个 pane **各有一套**「全选 + 永久删除」(用户 2026-10-10:「全选要拆成两部分」)。
668
+ 上面删的是一条记账、下面删的是真文件,合成一颗键就成了「勾了两条、不知道该删哪个」。
669
+ 批量条挂在 pane 里、在列表区之上,不跟着列表滚(列表滚走了也要够得着)。 */
670
+ .dsm-trash-pane-batch{flex:none;padding:7px 13px;border-bottom:1px solid var(--dsw-alias-border-l1)}
671
+ .dsm-trash-notice{flex:none}
672
+ .dsm-trash-pick{display:inline-flex;flex:none;align-items:center;padding:4px 1px;cursor:pointer}
673
+ .dsm-trash-pick input{width:15px;height:15px;margin:0;cursor:pointer;accent-color:var(--dsw-alias-state-error-primary)}
674
+ .dsm-trash-pick input:disabled{cursor:default}
675
+ /* 勾选过的那条给一层底色:勾了十几条之后要一眼看出勾的是哪几条。 */
676
+ .dsm-trash-item-picked{background:var(--dsw-alias-interactive-bg-hover);border-radius:8px}
677
+
619
678
  `
620
679
 
621
680
  /** 统一的异常文案:Error 取 message,其余(字符串/对象/null)原样 String。此前 63 处各写一遍。 */
@@ -1172,6 +1231,9 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1172
1231
  'history-import': 'compat.audit.op.sessionImport',
1173
1232
  'history-export': 'compat.audit.op.sessionExport',
1174
1233
  'history-workspace-register': 'compat.audit.op.sessionRegister',
1234
+ // 回收站保留期:与历史页的保留期分开两条文案 —— 一条管归档会话,一条管回收站,
1235
+ // 混着记会让流水读不出「改的到底是哪个页面的保留期」。
1236
+ 'trash-retention-set': 'compat.audit.op.trashRetention',
1175
1237
  }
1176
1238
  /** 每个操作走哪条路线:仅当有"原生委托"能力可用时才算原生。 */
1177
1239
  function operationRoutes(data) {
@@ -3404,13 +3466,35 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3404
3466
  "error.rules.reservedScene": "「{name}」是保留场景,不可{action}{reason}",
3405
3467
  "error.rules.singleSceneOnly": "除「全局」外同时只能启用一个场景{detail}",
3406
3468
  "error.agentsMd.sceneRebindFailed": "无法把场景「{scene}」的提示词绑定改成「{target}」:{reason}",
3407
- "error.agentsMd.referenced": "「{id}」仍被引用,不能删除:{refs}。先改掉引用(换绑提示词 / 退出场景 / 应用别的预设)再删除。", "error.agentsMd.refProbeFailed": "无法确认「{id}」是否被引用,已拒绝删除:引用探测失败(场景绑定 / AGENTS.md 当前内容 / 进场景前的基线,这三类里至少有一类没读到)。请稍后重试。",
3469
+ "error.agentsMd.referenced": "「{id}」不能删除:{refs}。先解除引用再删 —— 场景绑定的去场景页换绑提示词、或退出场景;正被应用的那份先点左边那颗开关「取消应用」。", "error.agentsMd.refProbeFailed": "无法确认「{id}」是否被引用,已拒绝删除:引用探测失败(场景绑定 / AGENTS.md 当前内容 / 进场景前的基线,这三类里至少有一类没读到)。请稍后重试。", "error.agentsMd.noUnapplyBackup": "没有可恢复的「应用之前的状态」:AGENTS.md 的当前内容不是由「应用」写进去的(例如首次打开时自动生成的那份预设),所以没有起点可回。要改掉文件里的内容:应用别的预设,或直接编辑 AGENTS.md 本身。", "error.agentsMd.unapplyWhileScene": "场景「{scene}」正在驱动全局基线,不能在这里取消应用:先退出场景,或到场景页改掉它的提示词绑定。",
3408
3470
  "error.source.reserved": "{origin}:不能停用或移除(里面的技能可以删除)",
3409
3471
  "action.rename": "改名", "action.lock": "锁定",
3410
3472
  "trash.title": "回收站", "trash.count.one": "{count} 个待处理技能", "trash.count.other": "{count} 个待处理技能", "trash.empty": "回收站为空", "trash.deletedAt": "删除于 {time}", "trash.source": "来源:{source}",
3411
3473
  "trash.btn.open": "回收站", "trash.items.count": "{count} 项", "trash.purge.confirm": "永久删除?",
3412
3474
  "trash.group.agents": "人设", "trash.group.scenes": "场景", "trash.group.presets": "提示词预设",
3413
3475
  "trash.restored": "已恢复「{name}」", "trash.purged": "已永久删除「{name}」", "trash.loadFailed": "读取回收站失败",
3476
+ // 保留期 + 批量删除(0.19.1)。保留期那几句刻意与 History 页的 hist.retention.* 分开:
3477
+ // 两处管的东西不同(归档会话 vs 回收站),文案合并会让用户以为改了这边那边也跟着变。
3478
+ "trash.retention.label": "保留期",
3479
+ "trash.retention.forever": "永久保留",
3480
+ "trash.retention.days": "{count} 天",
3481
+ "trash.retention.loading": "读取中…",
3482
+ "trash.retention.loadFailed": "读取保留期失败",
3483
+ "trash.retention.hint": "超期自动删除",
3484
+ "trash.retention.hintFull": "超过保留期的条目会被自动永久删除;修改设置会从此刻重新计时,不会立刻清空。",
3485
+ "trash.retention.saved": "已清理 {count} 项",
3486
+ "trash.retention.savedFailed": "已清理 {count} 项,{failed} 项失败(下一轮再试)",
3487
+ "trash.pick.title": "勾选",
3488
+ "trash.pane.dirs.pick.title": "只有「自定义目录」能永久删除记录;内置来源只能恢复读取",
3489
+ "trash.selected": "已选 {count} 项",
3490
+ "trash.selectAll": "全选", "trash.deselectAll": "取消全选",
3491
+ "trash.btn.purgeSelected": "永久删除 ({count})",
3492
+ "trash.purgeSelected.confirm": "永久删除所选的 {count} 个?不可恢复",
3493
+ "trash.btn.confirmPurgeMany": "确认删除 {count} 项",
3494
+ "trash.purgeMany.result": "已永久删除 {count} 个条目",
3495
+ "trash.purgeMany.partial": "已永久删除 {count} 个,{failed} 个失败:{reason}",
3496
+ "trash.forgetMany.result": "已删除 {count} 条来源记录",
3497
+ "trash.forgetMany.partial": "已删除 {count} 条来源记录,{failed} 条失败:{reason}",
3414
3498
  "confirm.trash.title": "移到回收站?", "confirm.trash.desc": "「{name}」将移入回收站,可恢复", "confirm.delete.title": "永久删除?", "confirm.delete.desc": "「{name}」将永久删除,无法恢复",
3415
3499
  "result.created": "已创建技能:{name}", "result.imported": "导入完成:{names}", "result.importPartial": "已导入:{imported};已跳过同名技能:{skipped}", "result.importSkipped": "未导入任何技能;已跳过同名技能:{names}", "result.importEmpty": "未导入任何技能。", "result.importWarnings": "{result};警告:{warnings}", "result.restored": "已恢复技能:{name}", "result.trashed": "已移到回收站:{name}", "result.deleted": "已永久删除:{name}", "result.updated": "状态已更新。", "result.preferred": "已改为生效:{name}({source})", "result.unpreferred": "已取消同名首选:{name}", "result.opened": "已用系统默认程序打开:{path}", "error.action": "操作失败:{error}",
3416
3500
  "warning.scan.truncated": "技能目录较大或嵌套过深,部分技能未显示:{path}", "warning.state.invalid": "状态文件不可读,已安全停用所有技能:{path}", "warning.backupUncleaned": "旧版本备份未清理:{path}({error})", "warning.project.unavailable": "活动会话的项目目录不可访问,项目技能未显示:{path}",
@@ -3418,7 +3502,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3418
3502
  "error.source.notFound": "路径不存在: {path}", "error.source.symlink": "不支持符号链接来源:{path}", "error.source.unrecognized": "无法识别的 skill 来源: {path}", "error.source.tooDeep": "skill 来源目录层级超过 {depth} 层: {path}",
3419
3503
  "error.import.overlap": "该目录与 DSH 技能目录重叠", "error.import.emptySource": "目录下没有 skill 条目:{path}", "error.import.invalidName": "名称不合法:{name}", "error.import.duplicateName": "批量来源中存在多个同名技能: {name}", "error.import.failed": "导入失败", "error.import.rollbackFailed": "覆盖导入回滚失败,备份保留在: {path}({error})",
3420
3504
  "error.upload.path": "上传内容包含非法路径:{path}", "error.upload.encoding": "上传内容编码无效", "error.upload.empty": "上传内容为空", "error.upload.tooMany": "上传文件过多,最多 {limit} 个", "error.upload.tooLarge": "上传内容过大,限制为 {limit} 字节", "error.upload.archiveTooLarge": "ZIP 压缩包过大,限制为 {limit} 字节", "error.upload.duplicate": "上传内容包含重复路径:{path}", "error.upload.zipInvalid": "ZIP 压缩包无法解压",
3421
- "error.trash.notFound": "回收站条目不存在: {id}", "error.trash.conflict": "无法恢复,同名技能已存在: {name}", "error.trash.invalid": "回收站条目路径非法: {id}", "error.trash.projectUnavailable": "原项目当前不在活动工作区中,无法恢复:{path}", "error.trash.rollbackFailed": "移入回收站回滚失败,未恢复内容保留在: {path}({error})",
3505
+ "error.trash.notFound": "回收站条目不存在: {id}", "error.trash.conflict": "无法恢复,同名技能已存在: {name}", "error.trash.invalid": "回收站条目路径非法: {id}", "error.trash.projectUnavailable": "原项目当前不在活动工作区中,无法恢复:{path}", "error.trash.rollbackFailed": "移入回收站回滚失败,未恢复内容保留在: {path}({error})", "error.trash.retention.invalid": "保留期需为非负整数(0=永久保留),收到: {value}", "error.trash.retention.writeFailed": "写入保留期失败: {reason}",
3422
3506
  "error.state.invalid": "技能管理器状态文件不可读,已拒绝覆盖:{path}",
3423
3507
  "error.create.descriptionRequired": "技能简介不能为空", "error.create.bodyRequired": "技能正文不能为空", "error.create.tooLarge": "技能内容过长", "error.create.conflict": "同名技能已存在: {name}",
3424
3508
  "error.proto.forbidden": "禁止的修改请求(缺少客户端标记)", "error.proto.forbiddenHost": "禁止的请求来源(非法 Host)", "error.proto.contentType": "请求体必须是 application/json", "error.proto.method": "不支持的请求方法", "error.proto.unknownAction": "未知操作", "error.proto.bodyTooLarge": "请求体过大", "error.proto.invalidJson": "请求体不是合法 JSON", "error.proto.nonJson": "服务端返回非 JSON 响应(HTTP {status})",
@@ -3550,7 +3634,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3550
3634
  "compat.audit.op.presetSave": "保存提示词", "compat.audit.op.quickSave": "保存快捷提示词", "compat.audit.op.quickSwitch": "启停快捷提示词", "compat.audit.op.quickDelete": "删除快捷提示词", "compat.audit.op.presetDelete": "删除提示词", "compat.audit.op.presetImport": "导入提示词", "compat.audit.op.presetApply": "应用提示词",
3551
3635
  "compat.audit.op.trashRestore": "从回收站恢复", "compat.audit.op.trashDelete": "永久删除",
3552
3636
  "compat.audit.op.toolTable": "模型工具表", "compat.audit.op.injectSettings": "注入设置", "compat.audit.op.sceneSettings": "场景页设置", "compat.audit.op.backupsClean": "清理旧备份",
3553
- "compat.audit.op.sessionArchive": "归档会话", "compat.audit.op.sessionUnarchive": "取消归档", "compat.audit.op.sessionDelete": "删除会话", "compat.audit.op.sessionRetention": "改保留策略", "compat.audit.op.sessionImport": "导入会话", "compat.audit.op.sessionExport": "导出会话", "compat.audit.op.sessionRegister": "登记工作区",
3637
+ "compat.audit.op.sessionArchive": "归档会话", "compat.audit.op.sessionUnarchive": "取消归档", "compat.audit.op.sessionDelete": "删除会话", "compat.audit.op.sessionRetention": "改保留策略", "compat.audit.op.trashRetention": "改回收站保留期", "compat.audit.op.sessionImport": "导入会话", "compat.audit.op.sessionExport": "导出会话", "compat.audit.op.sessionRegister": "登记工作区",
3554
3638
  "compat.backups.count": "现有 {count} 份,共 {size}",
3555
3639
  "compat.backups.del": "删除份数:", "compat.backups.delOption": "{count} 份",
3556
3640
  "compat.backups.level.global": "全局", "compat.backups.level.profile": "配置「{name}」", "compat.backups.level.legacy": "早期备份",
@@ -3951,7 +4035,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3951
4035
  "subagents.adv.legacy": ";旧格式:白名单 {allow} / 黑名单 {deny}",
3952
4036
  // 思考强度(0.14.0)。界面上只留"这一格现在能选什么"与**状态**(未选模型 / 拉取中 / 拉不到),
3953
4037
  // 不解释机制 —— 官方语义(改 provider/model 会丢掉继承值、不支持的档位直接拒)记在
3954
- // CHANGELOG / update.md,不搬进表单。needModel 保留但压到最短:它解释的是"这一格为什么灰着"。
4038
+ // CHANGELOG,不搬进表单。needModel 保留但压到最短:它解释的是"这一格为什么灰着"。
3955
4039
  "subagents.field.effort": "思考强度", "subagents.effort.default": "默认(模型的默认强度)", "subagents.effort.none": "默认", "subagents.effort.needModel": "先选模型", "subagents.effort.loading": "正在问该模型支持哪些档位…", "subagents.effort.unavailable": "该模型没有暴露可选档位,默认思考强度", "subagents.effort.dropped": "换模型后「{effort}」不在新模型的档位里,已清空。",
3956
4040
  // 来源下拉的空选项。措辞刻意与模型那条不同("继承主会话"):两者含义一致,但并排两个
3957
4041
  // 下拉出现同一句话会让人以为点错了格。这一句同时接住被撤掉的那条静态说明
@@ -3994,7 +4078,9 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3994
4078
  "import.none": "没有导入任何文件(全部被跳过)", "import.skipped": "已跳过:{items}",
3995
4079
  "error.import.noFiles": "没有选择要导入的文件",
3996
4080
  "nav.title": "工具",
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": "未知错误",
4081
+ "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.fileApplied": "它正被应用在 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 代)", "agm.unapply.title": "取消应用", "agm.unapply.body": "将把 ~/.dsh/AGENTS.md 恢复成开始应用提示词之前的那份内容(__last-applied__ 里保存的),此后不再有任何提示词预设生效。", "agm.unapply.note": "回到的是开始应用之前的那一份,中途应用过别的预设也一并撤销 —— 想换成其中某一份,直接点它自己的开关即可(效果等价)。这一步本身也会备份,可以再应用回来。", "agm.btn.confirmUnapply": "确认取消应用", "agm.unapply.switch.on": "AGENTS.md 里就是这一份 · 点一下取消应用(把 AGENTS.md 恢复成开始应用之前的内容)", "agm.unapply.switch.blocked": "AGENTS.md 里就是这一份,但没有「应用之前的状态」可恢复 —— 它不是由「应用」写进去的,所以关不掉。要改掉文件内容请应用别的预设或直接编辑 AGENTS.md;删除这份预设不会改动 AGENTS.md", "agm.unapply.switch.onRemove": "AGENTS.md 里就是这一份 · 点一下取消应用(开始应用之前没有 AGENTS.md,这一步会把它删掉)", "agm.unapply.bodyRemove": "开始应用提示词之前 ~/.dsh/AGENTS.md 并不存在,所以取消应用会把这个文件删掉(而不是写回内容),此后不再有任何提示词预设生效。", "agm.unapply.noteRemove": "删掉的是「应用」写进去的内容;这一份已备份到 __last-applied__(保留最近 5 代),需要时可以再应用回来。", "agm.btn.confirmUnapplyRemove": "确认取消应用并删除文件", "agm.result.unapplied": "已取消应用:AGENTS.md 已恢复成开始应用之前的内容", "agm.result.unappliedRemove": "已取消应用:AGENTS.md 已删除(开始应用之前它不存在)", "agm.unapply.unavailable": "AGENTS.md 的当前内容不是由「应用」写进去的(例如首次打开时自动生成的那份预设),没有「应用之前的状态」可恢复,所以关不掉。要改掉文件里的内容:应用别的预设,或直接编辑 AGENTS.md 本身;删除这份预设不会改动 AGENTS.md。", "agm.remove.fileNote": "注意:AGENTS.md 里仍然是这份内容,全局提示词照旧生效 —— 删除预设只把它移进回收站,不会改动 AGENTS.md。要让它不再生效,请改 AGENTS.md 本身或应用别的预设。", "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": "全局提示词",
4082
+ "scenes.field.prompt.none": "无(不绑定全局提示词)", "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": "还没有快捷提示词,先到「提示词」页新增",
4083
+ "scenes.field.quickPrompts.noneOption": "无(不设置快捷提示词)", "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": "未知错误",
3998
4084
  },
3999
4085
  en: {
4000
4086
  "title": "Skills", "desc": "Manage skills: enable, import, create, trash.", "link.project": "GitHub", "link.feedback": "Issues",
@@ -4047,13 +4133,33 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
4047
4133
  "error.rules.reservedScene": "「{name}」 is a reserved scene: {action} is not allowed{reason}",
4048
4134
  "error.rules.singleSceneOnly": "Only one scene besides 「global」 may be enabled at a time{detail}",
4049
4135
  "error.agentsMd.sceneRebindFailed": "Cannot change the prompt binding of scene 「{scene}」 to 「{target}」: {reason}",
4050
- "error.agentsMd.referenced": "「{id}」 is still referenced and cannot be deleted: {refs}. Remove those references first (rebind the prompt / leave the scene / apply another preset).", "error.agentsMd.refProbeFailed": "Cannot tell whether “{id}” is still referenced, so the delete was refused: the reference probe failed (at least one of scene bindings / the current AGENTS.md content / the pre-scene baseline could not be read). Please try again later.",
4136
+ "error.agentsMd.referenced": "“{id}” cannot be deleted: {refs}. Remove that reference first — for a scene binding, rebind the prompt on the scene page or leave the scene; for the one currently applied, flip the switch on the left to un-apply.", "error.agentsMd.refProbeFailed": "Cannot tell whether “{id}” is still referenced, so the delete was refused: the reference probe failed (at least one of scene bindings / the current AGENTS.md content / the pre-scene baseline could not be read). Please try again later.", "error.agentsMd.noUnapplyBackup": "There is no “state from before the apply” to restore: the current AGENTS.md was not written by “Apply” (the preset created automatically the first time you open this page is one such case), so there is no starting point to go back to. To change what AGENTS.md holds: apply another preset, or edit AGENTS.md itself.", "error.agentsMd.unapplyWhileScene": "Scene “{scene}” is driving the global baseline, so it cannot be un-applied here: leave the scene first, or change its prompt binding on the Scenes page.",
4051
4137
  "error.source.reserved": "{origin}: cannot be disabled or removed (the skills inside can still be deleted)",
4052
4138
  "action.rename": "rename", "action.lock": "locking",
4053
4139
  "trash.title": "Trash", "trash.count.one": "{count} skill pending", "trash.count.other": "{count} skills pending", "trash.empty": "Trash is empty", "trash.deletedAt": "Deleted {time}", "trash.source": "Source: {source}",
4054
4140
  "trash.btn.open": "Trash", "trash.items.count": "{count} item(s)", "trash.purge.confirm": "Delete forever?",
4055
4141
  "trash.group.agents": "Personas", "trash.group.scenes": "Scenes", "trash.group.presets": "Prompt presets",
4056
4142
  "trash.restored": "Restored “{name}”", "trash.purged": "Permanently deleted “{name}”", "trash.loadFailed": "Could not read the trash",
4143
+ "trash.retention.label": "Retention",
4144
+ "trash.retention.forever": "Keep forever",
4145
+ "trash.retention.days": "{count} days",
4146
+ "trash.retention.loading": "Loading…",
4147
+ "trash.retention.loadFailed": "Could not read the retention setting",
4148
+ "trash.retention.hint": "Auto-delete when expired",
4149
+ "trash.retention.hintFull": "Items older than the retention period are deleted forever automatically; changing this setting restarts the countdown from now, so nothing is wiped immediately.",
4150
+ "trash.retention.saved": "Cleaned up {count} item(s)",
4151
+ "trash.retention.savedFailed": "Cleaned up {count}; {failed} failed (will retry next round)",
4152
+ "trash.pick.title": "Select",
4153
+ "trash.pane.dirs.pick.title": "Only custom directories can be deleted; built-in sources can only be restored",
4154
+ "trash.selected": "{count} selected",
4155
+ "trash.selectAll": "Select all", "trash.deselectAll": "Deselect all",
4156
+ "trash.btn.purgeSelected": "Delete ({count})",
4157
+ "trash.purgeSelected.confirm": "Delete the {count} selected forever? This cannot be undone",
4158
+ "trash.btn.confirmPurgeMany": "Delete {count}",
4159
+ "trash.purgeMany.result": "Permanently deleted {count} item(s)",
4160
+ "trash.purgeMany.partial": "Permanently deleted {count}; {failed} failed: {reason}",
4161
+ "trash.forgetMany.result": "Removed {count} source record(s)",
4162
+ "trash.forgetMany.partial": "Removed {count} source record(s); {failed} failed: {reason}",
4057
4163
  "confirm.trash.title": "Move to trash?", "confirm.trash.desc": "“{name}” moves to trash and can be restored", "confirm.delete.title": "Delete forever?", "confirm.delete.desc": "“{name}” is deleted forever and cannot be recovered",
4058
4164
  "result.created": "Created skill: {name}", "result.imported": "Import complete: {names}", "result.importPartial": "Imported: {imported}; skipped existing skills: {skipped}", "result.importSkipped": "No skills were imported; existing skills were skipped: {names}", "result.importEmpty": "No skills were imported.", "result.importWarnings": "{result}; warnings: {warnings}", "result.restored": "Restored skill: {name}", "result.trashed": "Moved to trash: {name}", "result.deleted": "Permanently deleted: {name}", "result.updated": "Status updated.", "result.preferred": "Now active: {name} ({source})", "result.unpreferred": "Cleared the manual choice: {name}", "error.action": "Action failed: {error}",
4059
4165
  "warning.scan.truncated": "Some skills were not shown because the directory is too large or deeply nested: {path}", "warning.state.invalid": "The manager state file could not be read; all skills are disabled and state writes are blocked until it is repaired: {path}", "warning.backupUncleaned": "Old version backup was not cleaned up: {path} ({error})", "warning.project.unavailable": "The project directory of an active session is not accessible, so its project skills are hidden: {path}",
@@ -4061,7 +4167,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
4061
4167
  "error.source.notFound": "Path does not exist: {path}", "error.source.symlink": "Symlinked sources are not supported: {path}", "error.source.unrecognized": "Unrecognized skill source: {path}", "error.source.tooDeep": "Skill source directory depth exceeds {depth} levels: {path}",
4062
4168
  "error.import.overlap": "This folder overlaps the DSH skills folder", "error.import.emptySource": "No skill entries in this folder: {path}", "error.import.invalidName": "Invalid name: {name}", "error.import.duplicateName": "Batch source contains duplicate skill names: {name}", "error.import.failed": "Import failed", "error.import.rollbackFailed": "Overwrite import rollback failed; backups kept at: {path} ({error})",
4063
4169
  "error.upload.path": "Upload contains an invalid path: {path}", "error.upload.encoding": "Upload encoding is invalid", "error.upload.empty": "Upload is empty", "error.upload.tooMany": "Too many uploaded files; maximum {limit}", "error.upload.tooLarge": "Upload is too large; limit {limit} bytes", "error.upload.archiveTooLarge": "ZIP archive is too large; limit {limit} bytes", "error.upload.duplicate": "Upload contains a duplicate path: {path}", "error.upload.zipInvalid": "ZIP archive could not be extracted",
4064
- "error.trash.notFound": "Trash item not found: {id}", "error.trash.conflict": "Cannot restore because a skill with the same name exists: {name}", "error.trash.invalid": "Invalid trash item path: {id}", "error.trash.projectUnavailable": "The original project is not an active workspace, so this skill cannot be restored: {path}", "error.trash.rollbackFailed": "Move-to-trash rollback failed; unrecovered content was kept at: {path} ({error})",
4170
+ "error.trash.notFound": "Trash item not found: {id}", "error.trash.conflict": "Cannot restore because a skill with the same name exists: {name}", "error.trash.invalid": "Invalid trash item path: {id}", "error.trash.projectUnavailable": "The original project is not an active workspace, so this skill cannot be restored: {path}", "error.trash.rollbackFailed": "Move-to-trash rollback failed; unrecovered content was kept at: {path} ({error})", "error.trash.retention.invalid": "Retention must be a non-negative integer (0 = keep forever); got: {value}", "error.trash.retention.writeFailed": "Could not save the retention setting: {reason}",
4065
4171
  "error.state.invalid": "The manager state file could not be read, so overwriting it was refused: {path}",
4066
4172
  "error.create.descriptionRequired": "Skill description is required", "error.create.bodyRequired": "Skill body is required", "error.create.tooLarge": "Skill content is too large", "error.create.conflict": "A skill with the same name already exists: {name}",
4067
4173
  "error.proto.forbidden": "Forbidden mutation request (missing client marker)", "error.proto.forbiddenHost": "Forbidden request origin (invalid host)", "error.proto.contentType": "Content type must be application/json", "error.proto.method": "Method not allowed", "error.proto.unknownAction": "Unknown action", "error.proto.bodyTooLarge": "Request body too large", "error.proto.invalidJson": "Invalid JSON request body", "error.proto.nonJson": "Server returned a non-JSON response (HTTP {status})",
@@ -4193,7 +4299,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
4193
4299
  "compat.audit.op.presetSave": "Saved a prompt", "compat.audit.op.quickSave": "Saved a quick prompt", "compat.audit.op.quickSwitch": "Toggled a quick prompt", "compat.audit.op.quickDelete": "Deleted a quick prompt", "compat.audit.op.presetDelete": "Deleted a prompt", "compat.audit.op.presetImport": "Imported a prompt", "compat.audit.op.presetApply": "Applied a prompt",
4194
4300
  "compat.audit.op.trashRestore": "Restored from trash", "compat.audit.op.trashDelete": "Deleted forever",
4195
4301
  "compat.audit.op.toolTable": "Model tool table", "compat.audit.op.injectSettings": "Injection settings", "compat.audit.op.sceneSettings": "Scenes page settings", "compat.audit.op.backupsClean": "Cleaned old backups",
4196
- "compat.audit.op.sessionArchive": "Archived a session", "compat.audit.op.sessionUnarchive": "Unarchived a session", "compat.audit.op.sessionDelete": "Deleted a session", "compat.audit.op.sessionRetention": "Changed retention", "compat.audit.op.sessionImport": "Imported sessions", "compat.audit.op.sessionExport": "Exported sessions", "compat.audit.op.sessionRegister": "Registered a workspace",
4302
+ "compat.audit.op.sessionArchive": "Archived a session", "compat.audit.op.sessionUnarchive": "Unarchived a session", "compat.audit.op.sessionDelete": "Deleted a session", "compat.audit.op.sessionRetention": "Changed retention", "compat.audit.op.trashRetention": "Changed trash retention", "compat.audit.op.sessionImport": "Imported sessions", "compat.audit.op.sessionExport": "Exported sessions", "compat.audit.op.sessionRegister": "Registered a workspace",
4197
4303
  "compat.backups.count": "{count} copies, {size} in total",
4198
4304
  "compat.backups.del": "How many to delete:", "compat.backups.delOption": "{count}",
4199
4305
  "compat.backups.level.global": "Global", "compat.backups.level.profile": "Profile “{name}”", "compat.backups.level.legacy": "Older backups",
@@ -4593,7 +4699,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
4593
4699
  // Reasoning effort (0.14.0). The form keeps only what this field offers right now plus the
4594
4700
  // transient states (no model picked / loading / none available); the official semantics (the
4595
4701
  // 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.
4702
+ // rejected rather than clamped) live in CHANGELOG, not here.
4597
4703
  "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
4704
  // The blank entry of the provider dropdown. Deliberately worded differently from the model
4599
4705
  // one ("Inherit the main session"): same meaning, but the same sentence in two adjacent
@@ -4638,7 +4744,9 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
4638
4744
  "import.none": "Nothing was imported (all skipped)", "import.skipped": "Skipped: {items}",
4639
4745
  "error.import.noFiles": "No files selected",
4640
4746
  "nav.title": "Tools",
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",
4747
+ "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}. Remove that reference first — for a scene binding, rebind the prompt on the scene page or leave the scene; for the one currently applied, flip the switch on the left to un-apply.", "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.fileApplied": "it is applied in AGENTS.md (the one currently in effect)", "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)", "agm.unapply.title": "Un-apply", "agm.unapply.body": "This restores ~/.dsh/AGENTS.md to the content from before you started applying presets (kept in __last-applied__); no preset takes effect afterwards.", "agm.unapply.note": "What comes back is the content from before you started applying: presets you applied in between are rolled back too — to switch to one of them instead, just flip its own switch (same result). This step is backed up too, so you can apply it again.", "agm.btn.confirmUnapply": "Un-apply", "agm.unapply.switch.on": "This one is in AGENTS.md · click to un-apply (restore AGENTS.md to the content from before you started applying)", "agm.unapply.switch.blocked": "This one is in AGENTS.md, but there is no “state from before the apply” to restore — it was not written by “Apply”, so it cannot be turned off. To change the file, apply another preset or edit AGENTS.md directly; deleting this preset does not touch AGENTS.md", "agm.unapply.switch.onRemove": "This one is in AGENTS.md · click to un-apply (there was no AGENTS.md before you started applying, so this deletes the file)", "agm.unapply.bodyRemove": "There was no ~/.dsh/AGENTS.md before you started applying presets, so un-applying deletes that file instead of restoring content; no preset takes effect afterwards.", "agm.unapply.noteRemove": "What gets deleted is the content “Apply” wrote; that content is backed up to __last-applied__ (last 5 kept), so you can apply it back if needed.", "agm.btn.confirmUnapplyRemove": "Un-apply and delete the file", "agm.result.unapplied": "Un-applied: AGENTS.md is back to the content from before you started applying", "agm.result.unappliedRemove": "Un-applied: AGENTS.md was deleted (it did not exist before you started applying)", "agm.unapply.unavailable": "The current AGENTS.md was not written by “Apply” (the preset created automatically the first time you open this page is one such case), so there is no “state from before the apply” to restore and it cannot be turned off. To change what AGENTS.md holds: apply another preset, or edit AGENTS.md itself; deleting this preset does not touch AGENTS.md.", "agm.remove.fileNote": "Note: AGENTS.md still holds this content and your global prompt stays in effect — deleting the preset only moves it to the trash and does not touch AGENTS.md. To stop it taking effect, edit AGENTS.md itself or apply another preset.", "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": "Global prompt",
4748
+ "scenes.field.prompt.none": "None (no global prompt bound)", "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": "Custom", "scenes.field.quickPrompts.none": "No quick prompts yet — add some on the Prompts page",
4749
+ "scenes.field.quickPrompts.noneOption": "None (no quick prompts set)", "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 from the snapshot's text, so deleting this preset does not break the restore", "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",
4642
4750
  }
4643
4751
  };
4644
4752
 
@@ -5855,6 +5963,42 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
5855
5963
  else setTrash(function (cur) { return Object.assign({}, cur, { error: (res && res.error) || t('mcp.msg.failed') }) })
5856
5964
  }).catch(function (e) { setTrashBusy(false); setTrash(function (cur) { return Object.assign({}, cur, { error: errMsg(e) }) }) })
5857
5965
  }
5966
+ /**
5967
+ * 批量永久删除(回收站弹窗工具栏那一颗「永久删除 (N)」,先「全选」再删即等于清空)。
5968
+ *
5969
+ * 逐条调**已有的单条 op**(`agentsmd-trash-delete` / `quickprompt-trash-delete`):
5970
+ * 服务端不另造批量版 —— 那两条各自带着存在性校验,再造一份等于把判据抄两遍,
5971
+ * 而新 op 还要重新过一遍令牌与冻结登记(见 TrashModal 的注释)。
5972
+ *
5973
+ * 串行而不是并发:条目数天然有限(用户手删出来的),串行才给得出
5974
+ * 「删了几个、为什么没删掉」,并发只会让失败原因混成一团。
5975
+ */
5976
+ function purgePresetMany(items) {
5977
+ if (!items.length) return
5978
+ setTrashBusy(true)
5979
+ var done = 0, failed = 0, firstErr = null
5980
+ var step = function (i) {
5981
+ if (i >= items.length) {
5982
+ setTrashBusy(false)
5983
+ var text = failed === 0
5984
+ ? t('trash.purgeMany.result', { count: done })
5985
+ : t('trash.purgeMany.partial', { count: done, failed: failed, reason: firstErr || '' })
5986
+ setState(function (s) { return Object.assign({}, s, { notice: { kind: failed === 0 ? 'ok' : 'err', text: text } }) })
5987
+ loadTrash(true)
5988
+ return
5989
+ }
5990
+ apiCall(trashOp(items[i], 'delete'), { id: items[i].id }).then(function (res) {
5991
+ if (res && res.ok) done += 1
5992
+ else { failed += 1; if (!firstErr) firstErr = (res && res.error) || t('mcp.msg.failed') }
5993
+ step(i + 1)
5994
+ }).catch(function (e) {
5995
+ failed += 1
5996
+ if (!firstErr) firstErr = errMsg(e)
5997
+ step(i + 1)
5998
+ })
5999
+ }
6000
+ step(0)
6001
+ }
5858
6002
  // 导入:与技能页同一个 ImportModal(拖拽区 + 文件要求清单 + 结果反馈)。
5859
6003
  // 一次可选多份 .md,逐个读取正文导入;同名/失败逐条回报,不因一个失败中断其余的。
5860
6004
  // 两个提示词域共用这一颗按钮,类型在弹窗顶部选(`extra` 槽)—— 选完导入的是哪一档
@@ -5950,13 +6094,40 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
5950
6094
  // (把被手改的 AGENTS.md 写回场景绑定的内容)→ 仍放行。
5951
6095
  var sceneDriver = state.scenePrompt && state.scenePrompt.scene && state.scenePrompt.presetId && state.scenePrompt.missing !== true ? state.scenePrompt : null
5952
6096
  var sceneDriverLabel = sceneDriver ? (sceneDriver.label || sceneDriver.scene) : ''
6097
+ // 「取消应用」可不可用 = 宿主那边 `__last-applied__` 里有没有「链起点」
6098
+ // (`agentsmd-get-current` 回的 `unapplyAvailable`)。**播种出来的 default 没有记录**
6099
+ // —— 它内容与 AGENTS.md 相同、显示「生效中」,却从未发生过写入,所以关不掉;
6100
+ // 那种情况点开关只给一条说明,并把出路(应用别的预设 / 直接编辑 AGENTS.md)说清楚。
6101
+ var unapplyReady = !!(state.current && state.current.unapplyAvailable === true)
6102
+ // 取消应用会**删掉文件**(开始应用之前 AGENTS.md 根本不存在)—— 这是个不可逆感很强的动作,
6103
+ // 确认弹窗与开关悬停都必须提前说清,不能点下去才知道(用户 2026-10-10 反馈
6104
+ // 「取消应用,AGENTS.md 还是存在」,那次的成因是删文件这一支压根走不通)。
6105
+ var unapplyRemoves = unapplyReady && state.current.unapplyRemoves === true
5953
6106
  /** 引用说明:判定在宿主(scene-prompt-sync),这里只负责把它说成人话。 */
5954
6107
  function refText(r) {
5955
6108
  if (r.kind === 'scene') return t(r.active ? 'agm.ref.sceneActive' : 'agm.ref.scene', { scene: r.label || r.scene })
5956
6109
  if (r.kind === 'restore') return t('agm.ref.restore')
5957
- return t('agm.ref.file')
6110
+ // 两处 `t(...)` 分开写,**不要合成 `t(cond ? 'a' : 'b')`**:check-i18n 只认
6111
+ // `t('字面量')`,写成三元之后两个键都不再被统计,改名或打错字就没人拦得住了。
6112
+ return r.applied === true ? t('agm.ref.fileApplied') : t('agm.ref.file')
6113
+ }
6114
+ /**
6115
+ * **拦得住删除的**引用说明(其余引用由行上的标签承担)。
6116
+ *
6117
+ * 判据与宿主逐字一致(`ops/prompts.ts` 的 `agentsmd-remove`),两边必须同源 ——
6118
+ * 不同源就会出现「按钮点得动、点了却被拒」或反过来「按钮灰着、其实删得掉」。
6119
+ * ① 场景绑定 → 删了场景静默失效。
6120
+ * ② **当前应用的**那份(`file` + `applied`)→ 删了会留下一个用户看不懂的中间态:
6121
+ * AGENTS.md 内容一字不变、**照旧生效**,但界面上再没有预设对应它。用户看到的是
6122
+ * 「我把它删了,怎么全局提示词还在?」(2026-10-10 反馈),所以拦住并引导先取消应用。
6123
+ *
6124
+ * **`applied === false` 的 `file` 必须放行**:那是首次打开时自动播种出来的副本(内容与
6125
+ * AGENTS.md 相同、却从未被「应用」过),拦它没有任何出路 —— 它没有链起点可回,
6126
+ * 取消应用也关不掉,就是死路。
6127
+ */
6128
+ function blockingRefsOf(p) {
6129
+ return (p.refs || []).filter(function (r) { return r.kind === 'scene' || (r.kind === 'file' && r.applied === true) }).map(refText)
5958
6130
  }
5959
- function refsOf(p) { return (p.refs || []).map(refText) }
5960
6131
  // 结果提示的存续时长:**成功**短暂显示(与场景页的结果条同一节奏),
5961
6132
  // **失败常驻**到下一次操作为止 —— 这正是 Notice 的既定语义(「警告 / 错误需要
5962
6133
  // 用户处理,常驻到下一次操作」)。此前不分种类一律 4 秒消失,等于把唯一的线索
@@ -6001,11 +6172,38 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
6001
6172
  else refresh()
6002
6173
  }).catch(function (e) { setBusy(null); setApplyConfirm(null); actionFailed(e) })
6003
6174
  }
6175
+ /**
6176
+ * 取消应用:把全局基线恢复成**链起点**(宿主按 `__last-applied__` 里的记录办)——
6177
+ * 起点有内容就把那份写回去,起点文件不存在就把 AGENTS.md 删掉。**中途应用过别的预设
6178
+ * 也一并撤销**(一步回到起点,不是只退一步;见 `service.ts` 的 `backupGlobal`)。
6179
+ * 这是那颗「生效中」开关的**关**方向 —— 此前只有「应用」没有反向动作,于是
6180
+ * 「只有一份预设且它生效中」时开关关不掉、删除又被引用拦着(2026-10-10 实测反馈)。
6181
+ * 显示路径与 doApply 完全相同:失败走一次性横幅(不顶掉列表),成功重读页面。
6182
+ *
6183
+ * 成功时给一条**回执**(0.19.1,用户 2026-10-10 反馈「取消应用,AGENTS.md 还是存在」):
6184
+ * 原来只 `refresh()`,界面上唯一的变化是那颗开关灭了 —— 而"文件被删掉了"和"文件还在、
6185
+ * 只是内容换回去了"在界面上长得一模一样,用户只能自己去翻磁盘。回执按宿主回的
6186
+ * `removed` 分两种说法,把"到底发生了什么"直接写在页面上。
6187
+ */
6188
+ function doUnapply() {
6189
+ beginAction()
6190
+ setBusy('unapply')
6191
+ apiCall('agentsmd-unapply', {}).then(function (res) {
6192
+ setBusy(null); setApplyConfirm(null)
6193
+ if (res && res.ok === false) { actionFailed(res); return }
6194
+ setState(function (s) {
6195
+ // 同样分开写(见 refText 的说明):check-i18n 只统计字面量调用。
6196
+ return Object.assign({}, s, { notice: { kind: 'ok', text: (res && res.removed) ? t('agm.result.unappliedRemove') : t('agm.result.unapplied') } })
6197
+ })
6198
+ refresh()
6199
+ }).catch(function (e) { setBusy(null); setApplyConfirm(null); actionFailed(e) })
6200
+ }
6004
6201
  function doRemove(p) {
6005
6202
  beginAction()
6006
6203
  setBusy('rm-' + p.id)
6007
- // 宿主会拒绝删除「正在生效」的预设(按钮已禁用,这里兜住旧页面/竞态),
6008
- // 因此必须把响应里的错误显示出来,不能静默什么都不发生。
6204
+ // 删除只有**场景绑定**拦得住(宿主侧 agentsmd-remove 同一判据)。被场景绑定的那份
6205
+ // 按钮点得动但删不掉(点了当面列出「谁在用」),所以响应里的错误必须显示出来,
6206
+ // 不能静默什么都不发生。
6009
6207
  apiCall('agentsmd-remove', { id: p.id }).then(function (res) {
6010
6208
  setBusy(null); setApplyConfirm(null)
6011
6209
  if (res && res.ok === false) actionFailed(res)
@@ -6224,7 +6422,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
6224
6422
  var viaScene = p.activeVia === 'scene'
6225
6423
  var refs = p.refs || []
6226
6424
  var restoreRef = refs.some(function (r) { return r.kind === 'restore' })
6227
- var refTexts = refsOf(p)
6425
+ // 场景绑定与「当前应用的」那份拦得住删除(判据见 blockingRefsOf 的说明)。
6426
+ var blockingTexts = blockingRefsOf(p)
6228
6427
  // 场景绑的就是它,但 AGENTS.md 里的内容不是它(被手改过?)→ 说清楚,
6229
6428
  // 并留"再点一次那颗开关"这条路写回。
6230
6429
  var fileMismatch = !!sceneDriver && sceneDriver.presetId === p.id && !p.fileApplied
@@ -6247,24 +6446,40 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
6247
6446
  restoreRef ? React.createElement('span', { className: 'dsm-tag', title: t('agm.restore.tag.hint') }, t('agm.restore.tag')) : null),
6248
6447
  p.description ? React.createElement('div', { className: 'dsm-persona-desc', title: p.description }, p.description) : null),
6249
6448
  React.createElement('div', { className: 'dsm-source-actions' },
6250
- // 「应用 / 重新应用」换成与子智能体同一颗药丸开关(用户 2026-09-30 裁定):
6251
- // 那一档是单选,屏幕上永远只有一份生效中 —— 开关说的正是这件事,而两颗文字
6252
- // 按钮把"当前是哪份"留给标签去猜。关不掉是**语义**不是缺功能:基线没有
6253
- // "全部不应用"这一态,所以点已亮着的那颗 = 重新写回(把被手改的 AGENTS.md
6254
- // 拉回来),与旧「重新应用」同一个出口,确认弹窗也照旧。
6449
+ // 「应用 / 取消应用」同一颗药丸开关(0.19.1 起有两个方向):
6450
+ // 关 → 点 = 应用(把这份内容写进 AGENTS.md);
6451
+ // 开(AGENTS.md 里就是它)→ 点 = **取消应用**(把 AGENTS.md 恢复成
6452
+ // **开始应用提示词之前**的那一份 —— 中途应用过别的预设也一并撤销,
6453
+ // 不是只退一步;见 `service.ts` 的 `backupGlobal`)。此前这颗开关只有
6454
+ // 「应用」一个方向,用户实测「开启后无法关闭,也不能删除」—— 那是开关
6455
+ // 与删除保护一起构成的死锁。
6456
+ // 开但由**场景**驱动(`activeVia === 'scene'`)→ 点 = 应用(重新写回 /
6457
+ // 换绑场景);撤销归场景页管,本页不该把场景的绑定偷偷撤掉。
6458
+ // 没有链起点可回时(播种出来的 default 就是这样)不开弹窗,
6459
+ // 只给一条说明,并把出路(应用别的预设 / 直接编辑 AGENTS.md)说清楚。
6255
6460
  React.createElement(Switch, {
6256
6461
  on: p.active === true,
6257
6462
  disabled: busy !== null || state.anyLocked === true,
6258
6463
  label: t('agm.apply.switch') + ' ' + p.id,
6259
6464
  title: applyRebinds ? t('agm.apply.rebindScene', { scene: sceneDriverLabel })
6260
- : p.active ? t('agm.apply.switch.on') : t('agm.apply.hint'),
6261
- onClick: function () { setApplyConfirm({ id: p.id }) } }),
6465
+ : p.active
6466
+ ? (viaScene ? t('agm.apply.switch.on')
6467
+ : (unapplyReady ? (unapplyRemoves ? t('agm.unapply.switch.onRemove') : t('agm.unapply.switch.on')) : t('agm.unapply.switch.blocked')))
6468
+ : t('agm.apply.hint'),
6469
+ onClick: function () {
6470
+ if (!p.active || viaScene) { setApplyConfirm({ id: p.id }); return }
6471
+ if (!unapplyReady) { setState(function (s) { return Object.assign({}, s, { deny: t('agm.unapply.unavailable') }) }); return }
6472
+ setApplyConfirm({ id: p.id, unapply: true })
6473
+ } }),
6262
6474
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: state.anyLocked === true, onClick: function () { openEdit(p) } }, t('agm.btn.edit')),
6263
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy !== null || state.anyLocked === true, title: refTexts.length > 0 ? t('agm.btn.delete.blocked.refs', { refs: refTexts.join(t('agm.ref.sep')) }) : t('scenes.lock.blockedEdit'), onClick: function () {
6264
- // 被引用的预设**点得动但删不掉**:当面说出「谁在用」,而不是让用户对着
6265
- // 禁用按钮猜(disabled 的按钮连 title 都不弹)。宿主侧同样拒绝,这里只是先说。
6266
- if (refTexts.length > 0) { setState(function (s) { return Object.assign({}, s, { deny: t('agm.btn.delete.blocked.refs', { refs: refTexts.join(t('agm.ref.sep')) }) }) }); return }
6267
- setApplyConfirm({ id: p.id, remove: true })
6475
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy !== null || state.anyLocked === true, title: blockingTexts.length > 0 ? t('agm.btn.delete.blocked.refs', { refs: blockingTexts.join(t('agm.ref.sep')) }) : t('scenes.lock.blockedEdit'), onClick: function () {
6476
+ // 被引用中的预设**点得动但删不掉**:当面说出「谁在用 / 该怎么解除」,
6477
+ // 而不是让用户对着禁用按钮猜(disabled 的按钮连 title 都不弹)。
6478
+ // 宿主侧同一判据同样拒绝,这里只是先说。
6479
+ // 拦的是两类:场景绑定、以及**当前应用的**那份(后者删了 AGENTS.md
6480
+ // 照旧生效,会留下一个看不懂的中间态)。播种出来的副本不在其列。
6481
+ if (blockingTexts.length > 0) { setState(function (s) { return Object.assign({}, s, { deny: t('agm.btn.delete.blocked.refs', { refs: blockingTexts.join(t('agm.ref.sep')) }) }) }); return }
6482
+ setApplyConfirm({ id: p.id, remove: true, removeFileApplied: p.fileApplied === true })
6268
6483
  } }, t('agm.btn.delete')))))
6269
6484
  }))
6270
6485
 
@@ -6348,6 +6563,11 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
6348
6563
  { title: t('trash.group.presets'), sub: t('trash.section.presets.sub'), entries: trash.presets || [] },
6349
6564
  { title: t('trash.group.quick'), sub: t('trash.section.quick.sub'), entries: trash.quick || [] },
6350
6565
  ],
6566
+ // 上下两块各占一半、各自滚动、**每块自带一条**「已选 N 项 + 全选 + 永久删除 (N)」
6567
+ // (用户 2026-10-10:「提示词的回收站也要像技能回收站一样分成两部分」)。
6568
+ // 原先两块排在一条长列表里:35 条预设把 1 条快捷提示词压在滚不到底的下面,
6569
+ // 而顶部那一颗全局「永久删除」跨两块删 —— 勾完之后看不出会删掉哪几个。
6570
+ split: true,
6351
6571
  locked: state.anyLocked === true,
6352
6572
  loading: trash.loading,
6353
6573
  error: trash.error,
@@ -6355,6 +6575,9 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
6355
6575
  onClose: function () { setTrash(null) },
6356
6576
  onRestore: restorePreset,
6357
6577
  onPurge: purgePreset,
6578
+ // 批量删除 + 保留期改完后的重读(见 42-shared-ui.js 的 TrashModal / RetentionSelect)。
6579
+ onPurgeMany: purgePresetMany,
6580
+ onReload: function () { loadTrash(true) },
6358
6581
  }) : null,
6359
6582
  // 编辑:id 与内容都可改(改 id = 目录改名,场景绑定由宿主一起改名)。
6360
6583
  editModal ? React.createElement(Modal, { title: t('agm.edit.title') + ' · ' + editModal.id, closeLabel: t('btn.close'), onClose: function () { setEditModal(null) } },
@@ -6400,14 +6623,26 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
6400
6623
  createModal.error ? React.createElement('div', { className: 'dsm-feedback dsm-error' }, createModal.error) : null,
6401
6624
  React.createElement('div', { className: 'dsm-modal-actions' },
6402
6625
  React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy !== null || !String(createModal.id || '').trim(), onClick: function () { doCreate(createModal.id, createModal.content, createModal.description) } }, t('agm.btn.create')))) : null,
6403
- applyConfirm ? React.createElement(Modal, { title: applyConfirm.remove ? (t('agm.remove.title') + ' · ' + applyConfirm.id) : (t('agm.applyModal.title') + ' · ' + applyConfirm.id), closeLabel: t('btn.close'), onClose: function () { setApplyConfirm(null) } },
6626
+ // 三种确认共用一个弹窗:删除 / 取消应用 / 应用(`applyConfirm` 上的标记位区分)。
6627
+ applyConfirm ? React.createElement(Modal, { title: applyConfirm.remove ? (t('agm.remove.title') + ' · ' + applyConfirm.id) : ((applyConfirm.unapply ? t('agm.unapply.title') : t('agm.applyModal.title')) + ' · ' + applyConfirm.id), closeLabel: t('btn.close'), onClose: function () { setApplyConfirm(null) } },
6404
6628
  applyConfirm.remove
6405
- ? React.createElement('p', { className: 'dsm-help' }, t('agm.remove.title') + ' ' + applyConfirm.id + t('agm.remove.suffix'))
6406
- : React.createElement('div', null,
6407
- React.createElement('p', { className: 'dsm-help' }, t('agm.apply.prefix') + applyConfirm.id + t('agm.apply.suffix')),
6408
- React.createElement('div', { className: 'dsm-feedback dsm-warning' }, t('agm.apply.note'))),
6629
+ ? React.createElement('div', null,
6630
+ React.createElement('p', { className: 'dsm-help' }, t('agm.remove.title') + ' ' + applyConfirm.id + t('agm.remove.suffix')),
6631
+ // 删的正是「AGENTS.md 里就是它」的那一份 → 必须说清**文件不会被改动、
6632
+ // 全局提示词照旧生效**,否则用户会以为删除把全局提示词一起清掉了。
6633
+ // 注意这条只可能落在**播种出来的副本**上:真正「当前应用的」那份已经在
6634
+ // blockingRefsOf 里被拦住,根本走不到这个弹窗(用户 2026-10-10 的要求)。
6635
+ applyConfirm.removeFileApplied ? React.createElement('div', { className: 'dsm-feedback dsm-warning' }, t('agm.remove.fileNote')) : null)
6636
+ : applyConfirm.unapply
6637
+ ? React.createElement('div', null,
6638
+ // 删文件那一支必须换文案:说"恢复成开始应用之前的那份内容"会让用户以为文件还在。
6639
+ React.createElement('p', { className: 'dsm-help' }, unapplyRemoves ? t('agm.unapply.bodyRemove', { id: applyConfirm.id }) : t('agm.unapply.body', { id: applyConfirm.id })),
6640
+ React.createElement('div', { className: 'dsm-feedback dsm-warning' }, unapplyRemoves ? t('agm.unapply.noteRemove') : t('agm.unapply.note')))
6641
+ : React.createElement('div', null,
6642
+ React.createElement('p', { className: 'dsm-help' }, t('agm.apply.prefix') + applyConfirm.id + t('agm.apply.suffix')),
6643
+ React.createElement('div', { className: 'dsm-feedback dsm-warning' }, t('agm.apply.note'))),
6409
6644
  React.createElement('div', { className: 'dsm-modal-actions' },
6410
- React.createElement('button', { type: 'button', className: 'dsm-btn ' + (applyConfirm.remove ? 'dsm-btn-danger' : ''), disabled: busy !== null, onClick: function () { if (applyConfirm.remove) doRemove({ id: applyConfirm.id }); else doApply(applyConfirm.id) } }, applyConfirm.remove ? t('agm.btn.confirmRemove') : t('agm.btn.confirmApply')))) : null,
6645
+ React.createElement('button', { type: 'button', className: 'dsm-btn ' + (applyConfirm.remove || (applyConfirm.unapply && unapplyRemoves) ? 'dsm-btn-danger' : ''), disabled: busy !== null, onClick: function () { if (applyConfirm.remove) doRemove({ id: applyConfirm.id }); else if (applyConfirm.unapply) doUnapply(); else doApply(applyConfirm.id) } }, applyConfirm.remove ? t('agm.btn.confirmRemove') : (applyConfirm.unapply ? (unapplyRemoves ? t('agm.btn.confirmUnapplyRemove') : t('agm.btn.confirmUnapply')) : t('agm.btn.confirmApply'))))) : null,
6411
6646
  importOpen ? React.createElement(ImportModal, {
6412
6647
  key: 'imp', t: t, title: t('agm.btn.import'), accept: '.md',
6413
6648
  busy: importBusy, result: importResult,
@@ -7052,17 +7287,224 @@ function enabledMemoryCount(rules) {
7052
7287
  props.body)
7053
7288
  }
7054
7289
  /**
7055
- * 通用回收站弹窗:一行一个条目(名字 + 删除时间),右侧「恢复 / 永久删除」。
7290
+ * 回收站保留期选择器(0.19.1)。
7291
+ *
7292
+ * 自己拉、自己写:它是一个**全局设置**(`hub/trash-retention.json`),而回收站的界面
7293
+ * 散在五个页面(提示词 / 场景 / 子智能体 / 技能 / 记忆)。把状态放在每个页面的父组件里,
7294
+ * 等于同一段读-写-回执逻辑抄五遍,任何一遍漂了都会表现成「这页设了、那页没生效」。
7295
+ *
7296
+ * 回执写在选择器右边而不是弹窗顶部:用户改完这一格马上要看的是「这次清掉了什么」,
7297
+ * 而不是去别处找消息(清扫是异步发生的,没有回执就等于"改了个数字,什么也没看见")。
7298
+ *
7299
+ * 副作用面:`trash-retention-set` 成功后服务端会**立即扫一次**,可能真的永久删掉条目。
7300
+ * 所以调用方要传 `onReload` —— 否则弹窗里还列着已经不存在的条目,点恢复会报"条目不存在"。
7301
+ */
7302
+ function RetentionSelect(props) {
7303
+ var t = props.t
7304
+ var st = React.useState({ loading: true, days: null, busy: false, note: null, error: null })
7305
+ var s = st[0], set = st[1]
7306
+ React.useEffect(function () {
7307
+ var alive = true
7308
+ apiCall('trash-retention-get', {}).then(function (res) {
7309
+ if (!alive) return
7310
+ if (res && res.ok) set({ loading: false, days: Number(res.retentionDays) || 0, busy: false, note: null, error: null })
7311
+ else set({ loading: false, days: null, busy: false, note: null, error: (res && res.error) || t('trash.retention.loadFailed') })
7312
+ }).catch(function (e) {
7313
+ if (alive) set({ loading: false, days: null, busy: false, note: null, error: errMsg(e) })
7314
+ })
7315
+ return function () { alive = false }
7316
+ }, [])
7317
+ function choose(value) {
7318
+ var days = Number(value) || 0
7319
+ set(function (prev) { return Object.assign({}, prev, { busy: true, note: null, error: null }) })
7320
+ apiCall('trash-retention-set', { retentionDays: days }).then(function (res) {
7321
+ if (res && res.ok) {
7322
+ var swept = Number(res.swept) || 0
7323
+ var failed = Number(res.failed) || 0
7324
+ set({
7325
+ loading: false, days: Number(res.retentionDays) || 0, busy: false, error: null,
7326
+ // 只在**真的发生了事**的时候出回执:一条都没清掉时写「已保存;本次清理 0 项」
7327
+ // 是纯噪音(下拉框本身的值就是"已保存"的回执)。删除失败要如实说 ——
7328
+ // `failed > 0` 说明有条目删不掉(IO 错误),只报清掉几项会让用户以为回收站干净了。
7329
+ note: failed > 0
7330
+ ? t('trash.retention.savedFailed', { count: swept, failed: failed })
7331
+ : swept > 0 ? t('trash.retention.saved', { count: swept }) : null,
7332
+ })
7333
+ // 只在本轮真的删了东西时重读列表:没删东西还重读一次会让弹窗闪一下。
7334
+ if (swept > 0 && props.onReload) props.onReload()
7335
+ } else {
7336
+ set(function (prev) { return Object.assign({}, prev, { busy: false, error: translateError(t, res) }) })
7337
+ }
7338
+ }).catch(function (e) {
7339
+ set(function (prev) { return Object.assign({}, prev, { busy: false, error: errMsg(e) }) })
7340
+ })
7341
+ }
7342
+ // 选项与 History 页那套同形(永久保留 / 7 / 30 天),外加 90 天 —— 回收站里的东西
7343
+ // 比归档会话更常需要"留久一点再看"。`0` 是默认值,也是唯一不删任何东西的那一档。
7344
+ var PRESETS = [0, 7, 30, 90]
7345
+ var days = s.days
7346
+ // 侧车里的值可能是手改的、或来自更早的版本(例如 15)—— 不在预设里时把它补成一项,
7347
+ // 否则下拉框会显示成空白,用户会以为"根本没设置过"而重设一遍。
7348
+ var values = days !== null && PRESETS.indexOf(days) < 0 ? [days].concat(PRESETS) : PRESETS
7349
+ return React.createElement('div', { className: 'dsm-trash-retention' },
7350
+ React.createElement('span', { className: 'dsm-label' }, t('trash.retention.label')),
7351
+ React.createElement('select', {
7352
+ className: 'dsm-control',
7353
+ value: days === null ? '' : String(days),
7354
+ disabled: s.loading || s.busy || days === null,
7355
+ 'aria-label': t('trash.retention.label'),
7356
+ onChange: function (e) { choose(e.target.value) },
7357
+ },
7358
+ days === null ? React.createElement('option', { value: '' }, t('trash.retention.loading')) : null,
7359
+ values.map(function (v) {
7360
+ return React.createElement('option', { key: v, value: String(v) },
7361
+ v === 0 ? t('trash.retention.forever') : t('trash.retention.days', { count: v }))
7362
+ })),
7363
+ s.error ? React.createElement('span', { className: 'dsm-trash-hint dsm-trash-retention-err' }, s.error) : null,
7364
+ s.note ? React.createElement('span', { className: 'dsm-trash-hint' }, s.note) : null,
7365
+ // 说明只在**真的开了保留期**时出现:选着「永久保留」还写「超期自动删除」是自相矛盾的。
7366
+ // 文案本身压到 6 个字(用户 2026-10-10:「保留期一行就够了」),全文进 title ——
7367
+ // 这一行恒为一行,长句放不下的问题从源头消掉,而不是靠折行兜着。
7368
+ days !== null && days > 0
7369
+ ? React.createElement('span', { className: 'dsm-trash-hint', title: t('trash.retention.hintFull') }, t('trash.retention.hint'))
7370
+ : null)
7371
+ }
7372
+
7373
+ /**
7374
+ * 通用回收站弹窗:一行一个条目(勾选框 + 名字 + 删除时间),右侧「恢复 / 永久删除」。
7056
7375
  *
7057
7376
  * 子智能体 / 场景 / 提示词预设三处共用;永久删除就在本弹窗里就地二次确认,
7058
7377
  * 不再叠一层弹窗。恢复失败的原因显示在弹窗顶部 —— 不静默(同名已存在时必须说清)。
7378
+ *
7379
+ * 0.19.1 加的两件事:
7380
+ * · **保留期选择器**(`RetentionSelect`):回收站此前只有"进来"没有"出去"。
7381
+ * · **批量永久删除**:条目多的时候(本机实测场景回收站 68 条、记忆 120 条)
7382
+ * 一条条点等于没法用。勾选 + 「全选」+「永久删除 (N)」。
7383
+ *
7384
+ * 批量控件**只有一颗删除键**,与「全选」同处工具栏那一行、靠最右(用户 2026-10-10 裁定):
7385
+ * 原先并列「永久删除所选」与「清空回收站」两颗,两者是同一件事的两种范围,
7386
+ * 并列必然有一颗是废话 —— 要清空就先「全选」再删,两步。
7387
+ * 同理,勾选是**跨组**的(提示词页一次列两组),所以「全选」也必须是全局那一颗:
7388
+ * 每组各留一颗「全选」就等于每组也要各配一颗删除键,又变回多颗。
7389
+ *
7390
+ * 批量为什么走**前端循环调已有的单条删除 op**(`onPurgeMany` 由调用方实现):
7391
+ * 服务端那六条 `*-trash-delete` 各自带着自己的存在性校验与回滚,再写一条"批量版"
7392
+ * 就等于把六份判据复制一遍 —— 而且新 op 还要重新过一遍令牌/冻结登记。
7393
+ * 条数天然有限(用户手删出来的),循环没有性能问题。
7394
+ *
7395
+ * `split: true` = **分栏模式**(用户 2026-10-10:「提示词的回收站也要像技能回收站一样分成两部分」):
7396
+ * 两块上下各占一半、各自滚动、**每块自带一条**「已选 N 项 + 全选 + 永久删除 (N)」,
7397
+ * 大弹窗自己不滚。为什么不沿用顶部那一颗全局删除键:两块的后果不同(提示词页是
7398
+ * 「提示词预设目录」与「快捷提示词目录」),全局「全选 + 永久删除」要跨两块删 ——
7399
+ * 勾完之后根本看不出那一键会删掉哪几个。与技能回收站(43-skills.js 的 `trashPane`)
7400
+ * 同一套结构与类名,只是那份是手写的,这份做成了 `TrashModal` 的一个模式。
7059
7401
  */
7402
+ // 非分栏时那条唯一的批量条用这个哨兵当"确认中"的标记(分栏时用的是那一块的组标题)。
7403
+ // 用对象而不是字符串:组标题是用户可见文案,理论上可能撞上任何字符串字面量。
7404
+ const TRASH_BULK_ALL = { all: true }
7060
7405
  function TrashModal(props) {
7061
7406
  const t = props.t
7407
+ // 分栏模式:上下两块各占一半、各自滚动、**每块自带一条**批量条(见函数头注)。
7408
+ const split = props.split === true
7062
7409
  const c = React.useState(null)
7063
7410
  const confirmId = c[0], setConfirmId = c[1]
7064
- const row = function (item) {
7065
- const confirming = confirmId === item.id
7411
+ // 勾选集合。key = `<组标题>|<条目 id>`,**不能只按 id 记**:提示词页一次列两组
7412
+ // (全局预设 + 快捷提示词),两组的 id 来自同一条生成器,只按 id 记会让两组互相勾中。
7413
+ const sel = React.useState({})
7414
+ const selected = sel[0], setSelected = sel[1]
7415
+ // 批量二次确认。存的不是布尔而是"哪一条批量条在确认":分栏时上下两块**各有一条**,
7416
+ // 共用一颗布尔会让下面那块也跟着翻成确认态 —— 它的按钮并不是刚点的那颗。
7417
+ // 非分栏时用 `TRASH_BULK_ALL` 这个哨兵(整份列表只有一条批量条)。
7418
+ const bc = React.useState(null)
7419
+ const bulkConfirm = bc[0], setBulkConfirm = bc[1]
7420
+ // 组:默认就是调用方传的那一组(`groupTitle` / `entries`);给了 `groups` 就按数组列多组
7421
+ // —— 提示词页的回收站要一次看完"全局"与"快捷"两类,让用户先猜被删的东西在哪个域里,
7422
+ // 等于把恢复这条路堵在他自己手里。每组各自有空的说明,不合并成一句"什么都没删"。
7423
+ const groups = props.groups || [{ title: props.groupTitle, sub: props.groupSub, entries: props.entries || [] }]
7424
+ const rowKey = function (g, item) { return String(g.title) + '|' + String(item.id) }
7425
+ // 平铺一份:非分栏时「全选」与「永久删除」按**整份列表**算,不能只看某一组。
7426
+ const flat = []
7427
+ groups.forEach(function (g) {
7428
+ (g.entries || []).forEach(function (item) { flat.push({ group: g, item: item, key: rowKey(g, item) }) })
7429
+ })
7430
+ const allKeys = flat.map(function (x) { return x.key })
7431
+ // 场景锁定时批量删除一并禁用:单条「永久删除」已经禁用,批量是同一件事,
7432
+ // 只禁单条等于留了一个绕过锁定一次删光的口子。
7433
+ const batchDisabled = props.busy === true || props.locked === true
7434
+ // 没传 onPurgeMany 的调用方(目前没有,但 props 是可选的)不渲染批量控件,
7435
+ // 而不是渲染出按不动的按钮。
7436
+ const batchEnabled = typeof props.onPurgeMany === 'function' && flat.length > 0
7437
+ const toggle = function (key) {
7438
+ setSelected(function (prev) {
7439
+ var next = Object.assign({}, prev)
7440
+ if (next[key] === true) delete next[key]
7441
+ else next[key] = true
7442
+ return next
7443
+ })
7444
+ }
7445
+ const pickedOf = function (keys) { return keys.filter(function (k) { return selected[k] === true }) }
7446
+ const toggleAll = function (keys, on) {
7447
+ setSelected(function (prev) {
7448
+ var next = Object.assign({}, prev)
7449
+ keys.forEach(function (k) { if (on) next[k] = true; else delete next[k] })
7450
+ return next
7451
+ })
7452
+ }
7453
+ /**
7454
+ * 跑一条批量删除。`keys` 是这条批量条管的范围(非分栏 = 整份列表;分栏 = 那一块)。
7455
+ * 按**范围**过滤而不是直接拿整份勾选:分栏时上面那块点确认,不该把下面那块的勾选也删掉。
7456
+ * 删完也只清这一批的勾 —— 条目删掉之后 key 就失效了,留着会变成一串永远勾不中的残留;
7457
+ * 而另一块的勾选是用户还没处理的选择,不该替他清掉。
7458
+ */
7459
+ const runBulk = function (keys) {
7460
+ var inScope = {}
7461
+ keys.forEach(function (k) { inScope[k] = true })
7462
+ var items = flat.filter(function (x) { return inScope[x.key] === true && selected[x.key] === true }).map(function (x) { return x.item })
7463
+ setBulkConfirm(null)
7464
+ setSelected(function (prev) {
7465
+ var next = Object.assign({}, prev)
7466
+ keys.forEach(function (k) { delete next[k] })
7467
+ return next
7468
+ })
7469
+ if (items.length) props.onPurgeMany(items)
7470
+ }
7471
+ /**
7472
+ * 一条批量条:「已选 N 项 + 全选 + 永久删除 (N)」;二次确认就地替换这一行
7473
+ * (避免"确认了但按钮还在原位"的错觉)。非分栏与分栏共用,只是 `keys` 的范围不同。
7474
+ */
7475
+ const bulkRow = function (keys, confirmKey) {
7476
+ var pickedHere = pickedOf(keys)
7477
+ var allHere = keys.length > 0 && pickedHere.length === keys.length
7478
+ if (bulkConfirm === confirmKey) {
7479
+ return React.createElement('div', { className: 'dsm-trash-bulk dsm-trash-bulk-confirm' },
7480
+ React.createElement('span', { className: 'dsm-note' }, t('trash.purgeSelected.confirm', { count: pickedHere.length })),
7481
+ React.createElement('button', {
7482
+ type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: props.busy,
7483
+ onClick: function () { runBulk(keys) },
7484
+ }, t('trash.btn.confirmPurgeMany', { count: pickedHere.length })),
7485
+ React.createElement('button', {
7486
+ type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: props.busy,
7487
+ onClick: function () { setBulkConfirm(null) },
7488
+ }, t('btn.cancel')))
7489
+ }
7490
+ return React.createElement('div', { className: 'dsm-trash-bulk' },
7491
+ React.createElement('span', { className: 'dsm-note' }, t('trash.selected', { count: pickedHere.length })),
7492
+ // 「全选 ↔ 全不选」二合一(`bulkPair` 定宽,避免两种文案宽度不同把按钮推来推去)。
7493
+ React.createElement('button', {
7494
+ type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-bulk',
7495
+ disabled: batchDisabled,
7496
+ onClick: function () { toggleAll(keys, !allHere) },
7497
+ }, bulkPair(allHere ? t('trash.deselectAll') : t('trash.selectAll'), allHere ? t('trash.selectAll') : t('trash.deselectAll'))),
7498
+ React.createElement('button', {
7499
+ type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger',
7500
+ disabled: batchDisabled || pickedHere.length === 0,
7501
+ onClick: function () { setBulkConfirm(confirmKey) },
7502
+ }, t('trash.btn.purgeSelected', { count: pickedHere.length })))
7503
+ }
7504
+ const row = function (g, item) {
7505
+ const key = rowKey(g, item)
7506
+ const confirming = confirmId === key
7507
+ const isPicked = selected[key] === true
7066
7508
  const actions = confirming
7067
7509
  ? [
7068
7510
  React.createElement('span', { className: 'dsm-note', key: 'ask' }, t('trash.purge.confirm')),
@@ -7071,9 +7513,21 @@ function enabledMemoryCount(rules) {
7071
7513
  ]
7072
7514
  : [
7073
7515
  React.createElement('button', { key: 'restore', type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: props.busy || props.locked === true, onClick: function () { props.onRestore(item) } }, t('btn.restore')),
7074
- React.createElement('button', { key: 'purge', type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: props.busy || props.locked === true, onClick: function () { setConfirmId(item.id) } }, t('btn.delete.forever')),
7516
+ React.createElement('button', { key: 'purge', type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: props.busy || props.locked === true, onClick: function () { setConfirmId(key) } }, t('btn.delete.forever')),
7075
7517
  ]
7076
- return React.createElement('div', { className: 'dsm-trash-item', key: item.id },
7518
+ return React.createElement('div', { className: 'dsm-trash-item' + (isPicked ? ' dsm-trash-item-picked' : ''), key: item.id },
7519
+ // 勾选框只在能批量删的时候出现:没有 onPurgeMany 的调用方(见上面的 batchEnabled)
7520
+ // 渲染一颗永远禁用的勾选框,只会让人以为"这里本来能勾、只是现在不行"。
7521
+ batchEnabled
7522
+ ? React.createElement('label', { className: 'dsm-trash-pick', title: t('trash.pick.title') },
7523
+ React.createElement('input', {
7524
+ type: 'checkbox',
7525
+ checked: isPicked,
7526
+ disabled: batchDisabled,
7527
+ 'aria-label': t('trash.pick.title') + ' ' + String(item.name || ''),
7528
+ onChange: function () { toggle(key) },
7529
+ }))
7530
+ : null,
7077
7531
  React.createElement('div', { className: 'dsm-trash-main' },
7078
7532
  React.createElement('div', { className: 'dsm-name' }, item.name),
7079
7533
  React.createElement('div', { className: 'dsm-note' }, t('trash.deletedAt', {
@@ -7081,38 +7535,79 @@ function enabledMemoryCount(rules) {
7081
7535
  }))),
7082
7536
  actions)
7083
7537
  }
7084
- // 组:默认就是调用方传的那一组(`groupTitle` / `entries`);给了 `groups` 就按数组列多组
7085
- // —— 提示词页的回收站要一次看完"全局"与"快捷"两类,让用户先猜被删的东西在哪个域里,
7086
- // 等于把恢复这条路堵在他自己手里。每组各自有空的说明,不合并成一句"什么都没删"。
7087
- const groups = props.groups || [{ title: props.groupTitle, sub: props.groupSub, entries: props.entries || [] }]
7538
+ /**
7539
+ * 组头(标题 + 计数 + 后果说明)。分栏与非分栏共用同一套排版。
7540
+ *
7541
+ * 组头**不放按钮**:非分栏时勾选与删除都是跨组的(提示词页一次列两组),组头各放一颗
7542
+ * 「全选」就得各配一颗删除键 —— 那正是被砍掉的多颗形态;分栏时两颗动作下到每块自己的
7543
+ * 批量条里(`.dsm-trash-pane-batch`),组头仍然只是标题。
7544
+ */
7545
+ const groupHead = function (g) {
7546
+ const list = g.entries || []
7547
+ return React.createElement('div', { className: 'dsm-trash-group-head' },
7548
+ React.createElement('div', { className: 'dsm-trash-group-head-row' },
7549
+ React.createElement('span', { className: 'dsm-trash-group-title' }, g.title),
7550
+ React.createElement('span', { className: 'dsm-count' }, t('trash.items.count', { count: list.length }))),
7551
+ // 后果说明进组头(sticky 跟着滚):这组删的是记录还是文件,一眼可辨。
7552
+ g.sub ? React.createElement('p', { className: 'dsm-trash-group-sub' }, g.sub) : null)
7553
+ }
7554
+ /**
7555
+ * 分栏模式的一块(组头 + 批量条 + 列表区),与技能回收站(43-skills.js 的 `trashPane`)
7556
+ * 同一套结构与类名。批量条挂在**每块里**:两块的后果不同(预设目录 / 快捷提示词目录),
7557
+ * 一颗共用的「全选 + 永久删除」会让人勾完之后不知道那一键要删哪个。
7558
+ */
7559
+ const renderPane = function (g) {
7560
+ const list = g.entries || []
7561
+ const keys = (g.entries || []).map(function (item) { return rowKey(g, item) })
7562
+ const rows = props.loading
7563
+ ? React.createElement('div', { className: 'dsm-empty' }, t('memory.loading'))
7564
+ : list.length === 0
7565
+ ? React.createElement('div', { className: 'dsm-empty' }, t('trash.empty'))
7566
+ : list.map(function (item) { return row(g, item) })
7567
+ return React.createElement('div', { className: 'dsm-trash-group', key: String(g.title) },
7568
+ groupHead(g),
7569
+ // 批量条只在**这一块真有可删的东西**时出现:空块上挂一条「已选 0 项 + 全选 + 永久删除 (0)」
7570
+ // 是纯噪音,两块各占一半高度,那条还白吃掉一行的列表空间(技能页同一条判据)。
7571
+ batchEnabled && keys.length
7572
+ ? React.createElement('div', { className: 'dsm-trash-pane-batch' }, bulkRow(keys, String(g.title)))
7573
+ : null,
7574
+ React.createElement('div', { className: 'dsm-trash-pane-body' }, rows))
7575
+ }
7088
7576
  const renderGroup = function (g) {
7089
7577
  const list = g.entries || []
7090
7578
  const rows = props.loading
7091
7579
  ? React.createElement('div', { className: 'dsm-empty' }, t('memory.loading'))
7092
7580
  : list.length === 0
7093
7581
  ? React.createElement('div', { className: 'dsm-empty' }, t('trash.empty'))
7094
- : list.map(row)
7582
+ : list.map(function (item) { return row(g, item) })
7095
7583
  return React.createElement('div', { className: 'dsm-trash-group', key: String(g.title) },
7096
- React.createElement('div', { className: 'dsm-trash-group-head' },
7097
- React.createElement('div', { className: 'dsm-trash-group-head-row' },
7098
- React.createElement('span', { className: 'dsm-trash-group-title' }, g.title),
7099
- React.createElement('span', { className: 'dsm-count' }, t('trash.items.count', { count: list.length }))),
7100
- // 后果说明进组头(sticky 跟着滚):这组删的是记录还是文件,一眼可辨。
7101
- g.sub ? React.createElement('p', { className: 'dsm-trash-group-sub' }, g.sub) : null),
7584
+ groupHead(g),
7102
7585
  React.createElement('div', { className: 'dsm-trash-group-body' }, rows))
7103
7586
  }
7104
- // 列表类弹窗:宽高都固定(`.dsm-modal-list`),滚动只发生在 Modal 的 body 这一层 ——
7105
- // 条目数变化时弹窗不跳,也不会出现「弹窗滚 + 组内滚」的嵌套滚动条。
7587
+ // 批量那一行:「已选 N 项」占左侧,**「全选」与唯一的「永久删除」并排靠最右**
7588
+ // (用户 2026-10-10 裁定:删除键一个就够,与全选同一行、放最右边)。
7589
+ // 分栏时这一行只剩保留期 —— 批量条已经下到两块里,与技能回收站同形。
7590
+ const toolbar = split
7591
+ ? React.createElement(RetentionSelect, { key: 'retention', t: t, onReload: props.onReload })
7592
+ : React.createElement('div', { className: 'dsm-trash-toolbar', key: 'toolbar' },
7593
+ React.createElement(RetentionSelect, { t: t, onReload: props.onReload }),
7594
+ batchEnabled ? bulkRow(allKeys, TRASH_BULK_ALL) : null)
7595
+ // 列表类弹窗:宽高都固定(`.dsm-modal-list` + `.dsm-modal-trash`),滚动只发生在
7596
+ // Modal 的 body 这一层 —— 条目数变化时弹窗不跳,也不会出现「弹窗滚 + 组内滚」的嵌套滚动条。
7597
+ // 分栏时反过来:body 不滚,滚动下到每块的列表区里(`.dsm-modal-trash-split`)。
7106
7598
  return React.createElement(Modal, {
7107
7599
  key: 'trash-modal',
7108
7600
  title: props.title,
7109
7601
  closeLabel: t('btn.close'),
7110
7602
  list: true,
7111
- className: 'dsm-modal-list',
7603
+ className: 'dsm-modal-list dsm-modal-trash' + (split ? ' dsm-modal-trash-split' : ''),
7112
7604
  onClose: props.onClose,
7113
7605
  },
7114
7606
  props.error ? React.createElement(Notice, { kind: 'err', text: props.error }) : null,
7115
- groups.map(renderGroup))
7607
+ toolbar,
7608
+ split
7609
+ ? React.createElement('div', { className: 'dsm-trash-split' }, groups.map(renderPane))
7610
+ : groups.map(renderGroup))
7116
7611
  }
7117
7612
 
7118
7613
  /** 勾选行:复选框 + 名称/说明 + 右侧指标 + 可选行内动作。 */
@@ -7663,6 +8158,14 @@ function enabledMemoryCount(rules) {
7663
8158
  var us = react.useState(null), upload = us[0], setUpload = us[1];
7664
8159
  var ds = react.useState(null), detail = ds[0], setDetail = ds[1];
7665
8160
  var cs = react.useState({ path: "", label: "" }), customForm = cs[0], setCustomForm = cs[1];
8161
+ // 技能回收站的两个 pane **各有一套勾选**(见下方 modal === "trash"):上「不再读取的来源」
8162
+ // 删的是一条记账(磁盘一个字节不动),下「回收站里的技能」删的是真文件 —— 合成一套勾选,
8163
+ // 一颗删除键就不知道该删哪个了。所以 key 是 { dirs: {key:true}, skills: {id:true} }。
8164
+ var tps = react.useState({ dirs: {}, skills: {} }), trashSel = tps[0], setTrashSel = tps[1];
8165
+ // 就地二次确认(`'dirs' | 'skills' | null`):删的是不可恢复的东西,问一次再动手。
8166
+ var tcs = react.useState(null), trashConfirm = tcs[0], setTrashConfirm = tcs[1];
8167
+ var tbs = react.useState(false), trashBusy = tbs[0], setTrashBusy = tbs[1];
8168
+ var tns = react.useState(null), trashNotice = tns[0], setTrashNotice = tns[1];
7666
8169
  var ups = useUndoState(), undo = ups[0], setUndo = ups[1], dismissUndo = ups[2];
7667
8170
  var inflightRef = react.useRef(false);
7668
8171
  // 详情弹窗的请求序号(见 openDetail / closeModal):晚到的响应不得复活/顶掉弹窗内容。
@@ -8034,38 +8537,138 @@ function enabledMemoryCount(rules) {
8034
8537
  // 卡片结构复用共享回收站的 .dsm-trash-group(组头 + 计数 + 后果说明 + 行列表),观感与其它页一致。
8035
8538
  var skillTrashRows = data.trash || []
8036
8539
  var removedSourceRows = removedRoots || []
8037
- function trashPane(key, title, count, note, danger, rows, emptyText) {
8540
+ // 勾选只在**真能删**的行上出现:目录块里内置来源只有「恢复读取」(它们随时能被重新发现,
8541
+ // 删掉记账没有意义),只有 `custom === true` 的行有「永久删除」。给不可删的行渲染勾选框,
8542
+ // 勾了也删不掉,比没有勾选框更让人困惑 —— 所以可勾集合就是可删集合。
8543
+ var forgettableKeys = removedSourceRows.filter(function (r) { return r.custom === true }).map(function (r) { return r.key })
8544
+ var skillKeys = skillTrashRows.map(function (i) { return i.id })
8545
+ function pickedIn(pane) { return trashSel[pane] || {} }
8546
+ function togglePick(pane, key) {
8547
+ setTrashSel(function (prev) {
8548
+ var paneMap = Object.assign({}, prev[pane] || {})
8549
+ if (paneMap[key]) delete paneMap[key]; else paneMap[key] = true
8550
+ var next = Object.assign({}, prev)
8551
+ next[pane] = paneMap
8552
+ return next
8553
+ })
8554
+ }
8555
+ function setPanePicks(pane, keys, on) {
8556
+ setTrashSel(function (prev) {
8557
+ var paneMap = Object.assign({}, prev[pane] || {})
8558
+ keys.forEach(function (k) { if (on) paneMap[k] = true; else delete paneMap[k] })
8559
+ var next = Object.assign({}, prev)
8560
+ next[pane] = paneMap
8561
+ return next
8562
+ })
8563
+ }
8564
+ /**
8565
+ * 一个 pane 的批量动作:串行逐个走**已有的单条 op**(零新增 op)。
8566
+ *
8567
+ * 串行而不是并发:条目数天然有限(用户手删出来的),串行才给得出「删了几个、哪个没删掉」,
8568
+ * 并发只会让失败原因混成一团。失败不中断 —— 一条删不掉不该把其余十条一起卡住。
8569
+ */
8570
+ function runPaneBatch(pane, keys) {
8571
+ if (!keys.length) return
8572
+ var path = pane === "dirs" ? "/source-forget" : "/trash-delete"
8573
+ var resultKey = pane === "dirs" ? "trash.forgetMany" : "trash.purgeMany"
8574
+ setTrashBusy(true)
8575
+ setTrashConfirm(null)
8576
+ setTrashNotice(null)
8577
+ var done = 0, failed = 0, firstErr = null
8578
+ var step = function (i) {
8579
+ if (i >= keys.length) {
8580
+ setTrashBusy(false)
8581
+ setPanePicks(pane, keys, false)
8582
+ setTrashNotice({
8583
+ ok: failed === 0,
8584
+ text: failed === 0
8585
+ ? t(resultKey + ".result", { count: done })
8586
+ : t(resultKey + ".partial", { count: done, failed: failed, reason: firstErr || "" }),
8587
+ })
8588
+ refresh(true)
8589
+ return
8590
+ }
8591
+ var body = pane === "dirs" ? { root: keys[i] } : { id: keys[i] }
8592
+ callApi(path, { body: JSON.stringify(body) }).then(function () {
8593
+ done += 1
8594
+ step(i + 1)
8595
+ }).catch(function (e) {
8596
+ failed += 1
8597
+ if (!firstErr) firstErr = translateError(t, e)
8598
+ step(i + 1)
8599
+ })
8600
+ }
8601
+ step(0)
8602
+ }
8603
+ /**
8604
+ * 一个 pane(组头 + 批量条 + 列表区)。
8605
+ *
8606
+ * 批量条挂在**每个 pane 里**而不是弹窗顶部一颗:两块的后果不同(上=删记账,下=删文件),
8607
+ * 一颗共用的「全选 / 永久删除」会让人勾了两条之后不知道那一键要删哪个。用户 2026-10-10
8608
+ * 裁定:「全选要拆成两部分」。
8609
+ */
8610
+ function trashPane(key, paneId, title, count, note, danger, rows, emptyText, pickKeys, pickTitle) {
8611
+ var pickedMap = pickedIn(paneId)
8612
+ var picked = pickKeys.filter(function (k) { return pickedMap[k] === true })
8613
+ var allPicked = pickKeys.length > 0 && picked.length === pickKeys.length
8614
+ var batchRow = trashConfirm === paneId
8615
+ ? h("div", { className: "dsm-trash-bulk dsm-trash-bulk-confirm" },
8616
+ h("span", { className: "dsm-note" }, t("trash.purgeSelected.confirm", { count: picked.length })),
8617
+ h("button", { type: "button", className: "dsm-btn dsm-btn-quiet dsm-btn-danger", disabled: trashBusy, onClick: function () { runPaneBatch(paneId, picked); } }, t("trash.btn.confirmPurgeMany", { count: picked.length })),
8618
+ h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", disabled: trashBusy, onClick: function () { setTrashConfirm(null); } }, t("btn.cancel")))
8619
+ : h("div", { className: "dsm-trash-bulk" },
8620
+ h("span", { className: "dsm-note" }, t("trash.selected", { count: picked.length })),
8621
+ h("button", { type: "button", className: "dsm-btn dsm-btn-quiet dsm-btn-bulk", disabled: trashBusy || !pickKeys.length, title: pickTitle, onClick: function () { setPanePicks(paneId, pickKeys, !allPicked); } }, bulkPair(allPicked ? t("trash.deselectAll") : t("trash.selectAll"), allPicked ? t("trash.selectAll") : t("trash.deselectAll"))),
8622
+ h("button", { type: "button", className: "dsm-btn dsm-btn-quiet dsm-btn-danger", disabled: trashBusy || !picked.length, onClick: function () { setTrashConfirm(paneId); } }, t("trash.btn.purgeSelected", { count: picked.length })))
8038
8623
  return h("div", { key: key, className: "dsm-trash-group" + (danger ? " dsm-trash-group-danger" : "") },
8039
8624
  h("div", { className: "dsm-trash-group-head" },
8040
8625
  h("div", { className: "dsm-trash-group-head-row" },
8041
8626
  h("span", { className: "dsm-trash-group-title" }, title),
8042
8627
  h("span", { className: "dsm-count" }, count)),
8043
8628
  h("p", { className: "dsm-trash-group-sub" }, note)),
8629
+ // 批量条只在**这一块真有可勾的东西**时出现:空块上挂一条「已选 0 项 + 全选 + 永久删除 (0)」
8630
+ // 是纯噪音,而且两块各占一半高度、那条还白吃掉一行的列表空间。
8631
+ pickKeys.length ? h("div", { className: "dsm-trash-pane-batch" }, batchRow) : null,
8044
8632
  h("div", { className: "dsm-trash-pane-body" }, rows.length ? rows : h("div", { className: "dsm-empty" }, emptyText)))
8045
8633
  }
8046
- content.push(h(Modal, { key: "trash-modal", title: t("trash.title"), closeLabel: t("btn.close"), list: true, className: "dsm-modal-list dsm-modal-trash-split", onClose: function () { setModal(null); } },
8634
+ content.push(h(Modal, { key: "trash-modal", title: t("trash.title"), closeLabel: t("btn.close"), list: true, className: "dsm-modal-list dsm-modal-trash dsm-modal-trash-split", onClose: function () { setModal(null); setTrashConfirm(null); setTrashNotice(null); } },
8635
+ // 保留期选择器(0.19.1):技能回收站此前只能一条条点永久删除,而且技能域还留了一句
8636
+ // 「回收站没有自动清理,超龄条目通过 warnings 提示」的旧裁定 —— 保留期默认 `0`(永久保留)
8637
+ // 正是为了不与那条裁定冲突:只有用户在这里显式打开,才会自动清。
8638
+ h(RetentionSelect, { key: "retention", t: t, onReload: function () { refresh(false); } }),
8639
+ // 批量结果就地回报:页面级的那条 `result` 横幅被弹窗盖着,看不见等于没说。
8640
+ trashNotice ? h("div", { key: "trash-notice", className: "dsm-feedback dsm-trash-notice" + (trashNotice.ok ? "" : " dsm-error"), role: "alert" }, trashNotice.text) : null,
8047
8641
  h("div", { className: "dsm-trash-split" },
8048
- trashPane("pane-dirs", t("trash.section.dirs"), t(countKey("trash.dirs.count", removedSourceRows.length), { count: removedSourceRows.length }), t("trash.section.dirs.sub"), false,
8642
+ trashPane("pane-dirs", "dirs", t("trash.section.dirs"), t(countKey("trash.dirs.count", removedSourceRows.length), { count: removedSourceRows.length }), t("trash.section.dirs.sub"), false,
8049
8643
  removedSourceRows.map(function (root) {
8050
8644
  var name = rootDisplayName(t, root)
8051
- return h("div", { key: root.key, className: "dsm-trash-item" },
8645
+ var canForget = root.custom === true
8646
+ var picked = pickedIn("dirs")[root.key] === true
8647
+ return h("div", { key: root.key, className: "dsm-trash-item" + (picked ? " dsm-trash-item-picked" : "") },
8648
+ canForget ? h("label", { className: "dsm-trash-pick", title: t("trash.pick.title") },
8649
+ h("input", { type: "checkbox", checked: picked, disabled: trashBusy, "aria-label": t("trash.pick.title") + " " + name, onChange: function () { togglePick("dirs", root.key); } })) : null,
8052
8650
  h("div", { className: "dsm-trash-main" },
8053
8651
  h("div", { className: "dsm-name" }, name),
8054
8652
  h("div", { className: "dsm-note", title: root.path }, root.path)),
8055
- h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", disabled: busy, onClick: function () { post("/source-restore", { root: root.key }, "result.sourceRestored", { name: name }); } }, t("btn.source.restore")),
8056
- root.custom === true ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet dsm-btn-danger", disabled: busy, onClick: function () { setModal({ type: "source-forget-confirm", key: root.key, name: name, path: root.path }); } }, t("btn.source.forget")) : null)
8653
+ h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", disabled: busy || trashBusy, onClick: function () { post("/source-restore", { root: root.key }, "result.sourceRestored", { name: name }); } }, t("btn.source.restore")),
8654
+ canForget ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet dsm-btn-danger", disabled: busy || trashBusy, onClick: function () { setModal({ type: "source-forget-confirm", key: root.key, name: name, path: root.path }); } }, t("btn.source.forget")) : null)
8057
8655
  }),
8058
- t("trash.section.dirs.empty")),
8059
- trashPane("pane-skills", t("trash.section.skills"), t(countKey("trash.count", skillTrashRows.length), { count: skillTrashRows.length }), t("trash.section.skills.sub"), false,
8656
+ t("trash.section.dirs.empty"),
8657
+ forgettableKeys, t("trash.pane.dirs.pick.title")),
8658
+ trashPane("pane-skills", "skills", t("trash.section.skills"), t(countKey("trash.count", skillTrashRows.length), { count: skillTrashRows.length }), t("trash.section.skills.sub"), false,
8060
8659
  skillTrashRows.map(function (item) {
8061
- return h("div", { key: item.id, className: "dsm-trash-item" },
8660
+ var picked = pickedIn("skills")[item.id] === true
8661
+ return h("div", { key: item.id, className: "dsm-trash-item" + (picked ? " dsm-trash-item-picked" : "") },
8662
+ h("label", { className: "dsm-trash-pick", title: t("trash.pick.title") },
8663
+ h("input", { type: "checkbox", checked: picked, disabled: trashBusy, "aria-label": t("trash.pick.title") + " " + item.name, onChange: function () { togglePick("skills", item.id); } })),
8062
8664
  h("div", { className: "dsm-trash-main" },
8063
8665
  h("div", { className: "dsm-name" }, item.name),
8064
8666
  h("div", { className: "dsm-note" }, t("trash.deletedAt", { time: new Date(item.deletedAt).toLocaleString() }))),
8065
- h("button", { className: "dsm-btn dsm-btn-quiet", disabled: busy, onClick: function () { post("/trash-restore", { id: item.id }, "result.restored", { name: item.name }); } }, t("btn.restore")),
8066
- h("button", { className: "dsm-btn dsm-btn-quiet dsm-btn-danger", disabled: busy, onClick: function () { setModal({ type: "delete-confirm", id: item.id, name: item.name }); } }, t("btn.delete.forever")))
8667
+ h("button", { className: "dsm-btn dsm-btn-quiet", disabled: busy || trashBusy, onClick: function () { post("/trash-restore", { id: item.id }, "result.restored", { name: item.name }); } }, t("btn.restore")),
8668
+ h("button", { className: "dsm-btn dsm-btn-quiet dsm-btn-danger", disabled: busy || trashBusy, onClick: function () { setModal({ type: "delete-confirm", id: item.id, name: item.name }); } }, t("btn.delete.forever")))
8067
8669
  }),
8068
- t("trash.empty")))))
8670
+ t("trash.empty"),
8671
+ skillKeys, null))))
8069
8672
  }
8070
8673
  if (modal && modal.type === "source-forget-confirm") content.push(h(Modal, { key: "source-forget-confirm", title: t("confirm.source.forget.title"), closeLabel: t("btn.close"), onClose: function () { setModal("trash"); } }, h("p", { className: "dsm-desc" }, t("confirm.source.forget.desc", { name: modal.name })), h("p", { className: "dsm-help" }, t("confirm.source.forget.hint", { path: modal.path })), h("div", { className: "dsm-modal-actions" }, h("button", { className: "dsm-btn dsm-btn-danger", disabled: busy, onClick: function () { submitSourceForget(modal.key, modal.name); } }, t("btn.delete.forever")))));
8071
8674
  if (modal && modal.type === "trash-confirm") content.push(h(Modal, { key: "trash-confirm", title: t("confirm.trash.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("p", { className: "dsm-desc" }, t("confirm.trash.desc", { name: modal.name })), h("div", { className: "dsm-modal-actions" }, h("button", { className: "dsm-btn", disabled: busy, onClick: function () { post("/delete", { root: modal.root, name: modal.name }, "result.trashed", { name: modal.name }).then(function () { setModal(null); }).catch(swallowPostError); } }, t("btn.trash")))));
@@ -8429,7 +9032,15 @@ function enabledMemoryCount(rules) {
8429
9032
  { value: 0, label: t('hist.retention.forever') },
8430
9033
  { value: 7, label: t('hist.retention.days', { count: 7 }) },
8431
9034
  { value: 30, label: t('hist.retention.days', { count: 30 }) },
9035
+ { value: 90, label: t('hist.retention.days', { count: 90 }) },
8432
9036
  ]
9037
+ // 侧车里的值不在预设里时把它补成一项。`SourceSelect` 取不到就 `|| options[0]` 回退,
9038
+ // 于是「实际设的是 15 天」会显示成「永久保留」—— 是**静默误报**(用户以为不会自动删,
9039
+ // 其实会)。与回收站那套 RetentionSelect 同形;90 天之外用户仍可能从旧版本或手改带来别的值。
9040
+ var retentionDays = Number(data.retentionDays) || 0
9041
+ var retentionOpts = RETENTION_OPTS.some(function (o) { return o.value === retentionDays })
9042
+ ? RETENTION_OPTS
9043
+ : [{ value: retentionDays, label: t('hist.retention.days', { count: retentionDays }) }].concat(RETENTION_OPTS)
8433
9044
  var EXPORT_FORMAT_OPTS = [
8434
9045
  { value: 'markdown', label: 'Markdown' },
8435
9046
  { value: 'jsonl', label: 'JSONL' },
@@ -8562,7 +9173,7 @@ function enabledMemoryCount(rules) {
8562
9173
  React.createElement('input', { className: 'dsm-control dsm-search', type: 'text', placeholder: t('hist.search.placeholder'), value: query, onChange: function (e) { setQuery(e.target.value) } }),
8563
9174
  React.createElement('div', { className: 'dsm-source-filter' },
8564
9175
  React.createElement(SourceSelect, {
8565
- options: RETENTION_OPTS, value: data.retentionDays, onChange: function (v) { setRetention(Number(v)) },
9176
+ options: retentionOpts, value: data.retentionDays, onChange: function (v) { setRetention(Number(v)) },
8566
9177
  }))),
8567
9178
  // 走 Notice 而不是裸 div:令牌没过的提醒会在右侧自动多出一颗「填写令牌」按钮。
8568
9179
  data.error ? React.createElement(Notice, { kind: 'err', text: String(data.error) }) : null,
@@ -8866,6 +9477,31 @@ function enabledMemoryCount(rules) {
8866
9477
  else setTrash(function (cur) { return Object.assign({}, cur, { error: (res && res.error) || t('mcp.msg.failed') }) })
8867
9478
  }).catch(function (e) { setTrashBusy(false); setTrash(function (cur) { return Object.assign({}, cur, { error: errMsg(e) }) }) })
8868
9479
  }
9480
+ /** 批量永久删除:逐条调单条 op,串行(理由见 42-shared-ui.js 的 TrashModal 注释)。 */
9481
+ function purgeSceneMany(items) {
9482
+ if (!items.length) return
9483
+ setTrashBusy(true)
9484
+ var done = 0, failed = 0, firstErr = null
9485
+ var step = function (i) {
9486
+ if (i >= items.length) {
9487
+ setTrashBusy(false)
9488
+ setResult({
9489
+ ok: failed === 0,
9490
+ text: failed === 0
9491
+ ? t('trash.purgeMany.result', { count: done })
9492
+ : t('trash.purgeMany.partial', { count: done, failed: failed, reason: firstErr || '' }),
9493
+ })
9494
+ loadTrash(true)
9495
+ return
9496
+ }
9497
+ apiCall('scene-trash-delete', { id: items[i].id }).then(function (res) {
9498
+ if (res && res.ok) done += 1
9499
+ else { failed += 1; if (!firstErr) firstErr = (res && res.error) || t('mcp.msg.failed') }
9500
+ step(i + 1)
9501
+ }).catch(function (e) { failed += 1; if (!firstErr) firstErr = errMsg(e); step(i + 1) })
9502
+ }
9503
+ step(0)
9504
+ }
8869
9505
  React.useEffect(function () { if (!result || result.ok !== true) return undefined; var timer = setTimeout(function () { setResult(null) }, 2600); return function () { clearTimeout(timer) } }, [result])
8870
9506
  function refresh(silent) {
8871
9507
  if (!silent) setData(function (prev) { return Object.assign({}, prev, { loading: true, error: null }) })
@@ -8966,7 +9602,8 @@ function enabledMemoryCount(rules) {
8966
9602
  function loadPresetOptions(onLoaded) {
8967
9603
  apiCall('agentsmd-list', {}).then(function (res) {
8968
9604
  var list = (res && res.presets) || []
8969
- // 不再提供「不绑定」:候选就是预设本身,默认选中当前生效的那份。
9605
+ // 这里只放预设本身;表单里那颗下拉会在最前面补一条「无(不绑定提示词)」。
9606
+ // 默认选中当前生效的那份(用户裁定「默认就是当前启动的」)。
8970
9607
  var opts = []
8971
9608
  list.forEach(function (p) { opts.push({ value: p.id, label: p.active ? (p.id + ' · ' + t('scenes.prompt.activeTag')) : p.id }) })
8972
9609
  setPresetOptions(opts)
@@ -8981,14 +9618,29 @@ function enabledMemoryCount(rules) {
8981
9618
  setSceneForm(function (f) { return (f && String(f.prompt || '') !== '') ? f : Object.assign({}, f, { prompt: activeId }) })
8982
9619
  }
8983
9620
  // ── 场景建 / 改描述 / 改绑定的提示词 / 删 ──
8984
- // `quickPrompts` 三态:null = 「当前启用的」(这一域不绑、进出不碰);数组 = 按场景勾选
8985
- // (**空数组也是"绑了"**,意思是进入后全部停用)。表单里那颗下拉只给这两个方向。
8986
- function openCreateScene() { setSceneForm({ name: '', description: '', prompt: '', toolTablePreset: '', quickPrompts: null, error: null }); setModal({ type: 'scene-create' }); loadPresetOptions(defaultPromptToActive); loadToolTablePresets(); loadQuickOptions() }
9621
+ // 快捷提示词在表单里是**三选一**,落盘时映射成 `quickPrompts` 的三种取值:
9622
+ // 'current' → null(这一域不绑、进出不碰)/'none' → [](绑空表:进入后全部停用)
9623
+ // /'custom' → 勾选数组(**空数组也算 custom**)。
9624
+ //
9625
+ // 为什么表单要单独存一个 `quickMode`,而不是像原来那样从数组反推下拉值:
9626
+ // 「无」现在**不显示**「本场景启用」那张勾选卡(用户 2026-10-10),于是"数组为空"
9627
+ // 必须能分出两种来 —— 用户主动选了「无」,和用户选了「自定义」但一条都还没勾。
9628
+ // 反推的话后者会立刻翻成「无」、卡片当场消失,连勾第二条的机会都没有。
9629
+ //
9630
+ // 新建场景默认 'none'(用户 2026-10-10:「无应该是第一条,新建场景默认选择」)。
9631
+ // 口径与场景档案里另外三域一致:MCP / 技能 / 人设本来就是「一段都没建 = 该域全部停用」,
9632
+ // 新场景起手就是"一个都不启用",快捷提示词跟着走才不别扭;而 `null`(进出都不碰)
9633
+ // 留给 0.18.0 之前建的老场景,编辑时按档案原样回填。
9634
+ function openCreateScene() { setSceneForm({ name: '', description: '', prompt: '', toolTablePreset: '', quickMode: 'none', quickPrompts: [], error: null }); setModal({ type: 'scene-create' }); loadPresetOptions(defaultPromptToActive); loadToolTablePresets(); loadQuickOptions() }
8987
9635
  function openEditScene(scene) {
8988
9636
  var sceneArchive = (data.archives || {})[scene.name] || {}
8989
9637
  setSceneForm({
8990
9638
  name: scene.name, description: scene.description || '', prompt: scene.prompt || '',
8991
9639
  toolTablePreset: typeof sceneArchive.toolTablePreset === 'string' ? sceneArchive.toolTablePreset : '',
9640
+ // 档案里没有 quickPrompts 段 = 0.18.0 之前的老场景 = 'current'(进出不碰);
9641
+ // 空数组 = 'none';非空 = 'custom'。落盘的值与这里一一对应。
9642
+ quickMode: !Array.isArray(sceneArchive.quickPrompts)
9643
+ ? 'current' : (sceneArchive.quickPrompts.length ? 'custom' : 'none'),
8992
9644
  quickPrompts: Array.isArray(sceneArchive.quickPrompts) ? sceneArchive.quickPrompts.slice() : null,
8993
9645
  error: null,
8994
9646
  })
@@ -9014,8 +9666,11 @@ function enabledMemoryCount(rules) {
9014
9666
  var toolTablePreset = String(sceneForm.toolTablePreset || '')
9015
9667
  // 快捷提示词:null = 解绑(回到"保持现状"),数组 = 勾选集(空数组是"一条都不勾")。
9016
9668
  // 两个方向都必须**显式送出去**,不能省略 —— 省略在服务端那边是"这一域不碰"。
9017
- var quickPrompts = sceneForm.quickPrompts === null || sceneForm.quickPrompts === undefined
9018
- ? null : (sceneForm.quickPrompts || []).slice()
9669
+ // 以 `quickMode` 为准而不是从数组反推:'custom' 且一条都没勾时数组是空的,
9670
+ // 那要落成 `[]`(= 进入后全部停用)—— 与「无」落盘的值一样。两者在磁盘上本就
9671
+ // 不可区分(都是"一条都不启用"),区分只在表单里:要不要显示那张勾选卡。
9672
+ var quickPrompts = sceneForm.quickMode === 'current' ? null
9673
+ : (sceneForm.quickMode === 'custom' ? (sceneForm.quickPrompts || []).slice() : [])
9019
9674
  var payload = isEdit
9020
9675
  ? { name: originalName, nextName: name, description: description, prompt: prompt, toolTablePreset: toolTablePreset, quickPrompts: quickPrompts }
9021
9676
  : { name: name, description: description, prompt: prompt, toolTablePreset: toolTablePreset, quickPrompts: quickPrompts }
@@ -9993,33 +10648,60 @@ function enabledMemoryCount(rules) {
9993
10648
  React.createElement('div', { className: 'dsm-field' },
9994
10649
  React.createElement('span', { className: 'dsm-label' }, t('scenes.field.prompt')),
9995
10650
  presetOptions.length > 0
9996
- ? React.createElement(SourceSelect, { options: presetOptions, value: sceneForm.prompt || '', onChange: function (v) { setSceneForm(Object.assign({}, sceneForm, { prompt: v })) } })
10651
+ // 「无」排在第一位,是**不绑定**这一态在表单里唯一的入口(用户 2026-10-10 要求)。
10652
+ // 此前这里只有预设本身,于是「选了某份预设之后想改回不绑」在表单里无路可走。
10653
+ // 语义上是安全的:`prompt` 为空时 `resolveScenePreset()` 直接跳过这个场景,
10654
+ // 而 `sync()` 在没有场景驱动时会把进场景前的基线写回去 —— 从「绑了」切到
10655
+ // 「无」不会把上一份预设留在 AGENTS.md 里。
10656
+ // 候选用 `presetOptions` 而不是「无 + presetOptions」判空:一份预设都没有时
10657
+ // 该说的是「先去建一份」,而不是给一颗只有「无」的下拉(默认本来就是无)。
10658
+ ? React.createElement(SourceSelect, {
10659
+ options: [{ value: '', label: t('scenes.field.prompt.none') }].concat(presetOptions),
10660
+ value: sceneForm.prompt || '',
10661
+ onChange: function (v) { setSceneForm(Object.assign({}, sceneForm, { prompt: v })) },
10662
+ })
9997
10663
  : React.createElement('p', { className: 'dsm-help' }, t('scenes.prompt.noPresets')),
9998
10664
  helpBullets(t, 'scenes.field.prompt.hint')),
9999
10665
  // 快捷提示词(0.18.0):默认「当前启用的」= 这一域**不绑**,进出不碰它;
10000
- // 切到「按场景勾选」才给清单 —— 勾进来的进入本场景后启用、没勾的停用,
10666
+ // 切到「自定义」才给清单 —— 勾进来的进入本场景后启用、没勾的停用,
10001
10667
  // 退出场景按进场景前那批开关还原(与子智能体那一档同一条机制)。
10002
10668
  // 空表是"绑了但一条都不勾"(进入后全部停用),与"不绑"是相反的意思,
10003
- // 所以那颗下拉只给两个方向,不让用户靠"清空"去表达"不绑"。
10669
+ // 所以这里是**三态**而不是两态:第三条「无」正是那个空表(用户 2026-10-10 要求)。
10670
+ // 下拉显示的就是 `quickMode` 本身(不从数组反推,理由见 openCreateScene 上面)。
10004
10671
  React.createElement('div', { className: 'dsm-field' },
10005
10672
  React.createElement('span', { className: 'dsm-label' }, t('scenes.field.quickPrompts')),
10006
10673
  React.createElement(SourceSelect, {
10007
- value: Array.isArray(sceneForm.quickPrompts) ? 'custom' : '',
10008
- options: [{ value: '', label: t('scenes.field.quickPrompts.current') }, { value: 'custom', label: t('scenes.field.quickPrompts.custom') }],
10674
+ value: sceneForm.quickMode || 'current',
10675
+ // 「无」排第一(用户 2026-10-10:「无应该是第一条」)—— 与全局提示词那一格
10676
+ // 同一条排法,把"新建场景会看到的那个"放在最上面。
10677
+ options: [
10678
+ { value: 'none', label: t('scenes.field.quickPrompts.noneOption') },
10679
+ { value: 'current', label: t('scenes.field.quickPrompts.current') },
10680
+ { value: 'custom', label: t('scenes.field.quickPrompts.custom') },
10681
+ ],
10009
10682
  onChange: function (v) {
10010
10683
  setSceneForm(Object.assign({}, sceneForm, {
10011
- quickPrompts: v === 'custom'
10012
- // 切过来时从**当前开着的那些**起步(用户裁定:默认就是当前启用的),
10013
- // 而不是给一张空表 —— 空表说的是"进入后全部停用"。
10014
- ? (quickOptions || []).filter(function (p) { return p.enabled !== false }).map(function (p) { return p.id })
10015
- : null,
10684
+ quickMode: v,
10685
+ quickPrompts: v === 'none'
10686
+ // 「无」= 绑一张空表:进入后这一域全部停用,退出时按进场景前的开关还原。
10687
+ ? []
10688
+ : v === 'custom'
10689
+ // 切过来时从**当前开着的那些**起步(用户裁定:默认就是当前启用的),
10690
+ // 而不是给一张空表 —— 空表说的是"进入后全部停用"。一条都没开时起步
10691
+ // 就是空表,但模式仍是 'custom',卡片照常显示、勾得上。
10692
+ ? (quickOptions || []).filter(function (p) { return p.enabled !== false }).map(function (p) { return p.id })
10693
+ : null,
10016
10694
  }))
10017
10695
  },
10018
10696
  }),
10019
10697
  // 勾选清单是一张段卡片(与档案编辑器同一套排版):段头报"已勾几条 / 共几条"并给
10020
10698
  // 全选,段体按内容长(超过 6 行才内滚)。原先这里是一块裸的固定 216px 滚动体,
10021
10699
  // 一条也占 216px —— 弹窗中间凭空一片空白(用户 2026-10-01 反馈)。
10022
- Array.isArray(sceneForm.quickPrompts)
10700
+ //
10701
+ // 只在「自定义」时出现(用户 2026-10-10:「快捷提示词是无时,本场景启用不要显示」):
10702
+ // 「无」说的是"这个场景不设置快捷提示词",底下再挂一张全空的勾选卡是自相矛盾;
10703
+ // 「当前启用的(保持现状)」本来就一个字节都不碰,更没有可勾的。
10704
+ sceneForm.quickMode === 'custom'
10023
10705
  ? (quickOptions || []).length
10024
10706
  ? seg({
10025
10707
  title: t('scenes.field.quickPrompts.list'),
@@ -10098,6 +10780,8 @@ function enabledMemoryCount(rules) {
10098
10780
  onClose: function () { setTrash(null) },
10099
10781
  onRestore: restoreScene,
10100
10782
  onPurge: purgeScene,
10783
+ onPurgeMany: purgeSceneMany,
10784
+ onReload: function () { loadTrash(true) },
10101
10785
  }) : null,
10102
10786
  )
10103
10787
  }
@@ -10176,6 +10860,31 @@ function enabledMemoryCount(rules) {
10176
10860
  else setTrash(function (cur) { return Object.assign({}, cur, { error: (res && res.error) || t('mcp.msg.failed') }) })
10177
10861
  }).catch(function (e) { setTrashBusy(false); setTrash(function (cur) { return Object.assign({}, cur, { error: errMsg(e) }) }) })
10178
10862
  }
10863
+ /** 批量永久删除:逐条调单条 op,串行(理由见 42-shared-ui.js 的 TrashModal 注释)。 */
10864
+ function purgePersonaMany(items) {
10865
+ if (!items.length) return
10866
+ setTrashBusy(true)
10867
+ var done = 0, failed = 0, firstErr = null
10868
+ var step = function (i) {
10869
+ if (i >= items.length) {
10870
+ setTrashBusy(false)
10871
+ setResult({
10872
+ ok: failed === 0,
10873
+ text: failed === 0
10874
+ ? t('trash.purgeMany.result', { count: done })
10875
+ : t('trash.purgeMany.partial', { count: done, failed: failed, reason: firstErr || '' }),
10876
+ })
10877
+ loadTrash(true)
10878
+ return
10879
+ }
10880
+ apiCall('subagent-trash-delete', { id: items[i].id }).then(function (res) {
10881
+ if (res && res.ok) done += 1
10882
+ else { failed += 1; if (!firstErr) firstErr = (res && res.error) || t('mcp.msg.failed') }
10883
+ step(i + 1)
10884
+ }).catch(function (e) { failed += 1; if (!firstErr) firstErr = errMsg(e); step(i + 1) })
10885
+ }
10886
+ step(0)
10887
+ }
10179
10888
  React.useEffect(function () { if (!result || result.ok !== true) return undefined; var timer = setTimeout(function () { setResult(null) }, 2600); return function () { clearTimeout(timer) } }, [result])
10180
10889
  function refresh(silent) {
10181
10890
  if (!silent) setData(function (prev) { return Object.assign({}, prev, { loading: true, error: null }) })
@@ -11059,6 +11768,8 @@ function enabledMemoryCount(rules) {
11059
11768
  onClose: function () { setTrash(null) },
11060
11769
  onRestore: restorePersona,
11061
11770
  onPurge: purgePersona,
11771
+ onPurgeMany: purgePersonaMany,
11772
+ onReload: function () { loadTrash(true) },
11062
11773
  }) : null)
11063
11774
  }
11064
11775
 
@@ -11824,6 +12535,10 @@ function enabledMemoryCount(rules) {
11824
12535
  React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy || editorNameInvalid || editorGroupInvalid || editorDescLen > 500 || !String(editor.name || '').trim() || !String(editor.body || '').trim(), onClick: editor.mode === 'create' ? submitCreate : submitUpdate }, editor.mode === 'create' ? t('memory.btn.create') : t('memory.btn.save')))) : null,
11825
12536
  // 场景建/删/档案弹窗已移至「场景」页(ScenesPage)。
11826
12537
  modal && modal.type === 'trash' ? React.createElement(Modal, { key: 'trash', wide: true, title: t('memory.trash.title'), closeLabel: t('btn.close'), onClose: function () { setModal(null) } },
12538
+ // 保留期选择器(0.19.1):记忆回收站是本机条目最多的一处(实测 120 条),
12539
+ // 没有保留期就只能一条条点永久删除。放在 loading 判断**之前** ——
12540
+ // 它有自己的加载状态,不该跟着列表一起消失。
12541
+ React.createElement(RetentionSelect, { key: 'retention', t: t, onReload: loadTrash }),
11827
12542
  trash.loading ? React.createElement('div', { className: 'dsm-empty' }, t('memory.loading'))
11828
12543
  : React.createElement(React.Fragment, null,
11829
12544
  trash.error ? React.createElement('div', { className: 'dsm-feedback dsm-error', role: 'alert' }, String(trash.error)) : null,
@@ -12661,6 +13376,12 @@ function enabledMemoryCount(rules) {
12661
13376
  // 常量用函数包一层:测试会把 _pages 的每个值当组件渲染一遍,数字会被 React 当成非法元素类型报警告。
12662
13377
  _pages.sceneDescMax = function () { return SCENE_DESC_MAX }
12663
13378
  _pages.memDescMax = function () { return MEM_DESC_MAX }
13379
+ // 回收站弹窗与保留期选择器(0.19.1):它们不是页面,但**四个页面共用**
13380
+ // (提示词 / 场景 / 子智能体走 TrashModal,技能页与记忆页各挂一个 RetentionSelect)。
13381
+ // 导出是为了能脱离宿主单独渲染一次 —— 这类"工具栏 + 固定高度列表"的排版
13382
+ // 只有真渲染才看得出挤不挤(同 `_pages.VersionBadge` 那条理由)。
13383
+ _pages.TrashModal = TrashModal
13384
+ _pages.RetentionSelect = RetentionSelect
12664
13385
  },
12665
13386
  }
12666
13387
  return module.exports