dsh-plugin-tool-management 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +179 -0
  2. package/README.md +117 -221
  3. package/README_EN.md +116 -220
  4. package/docs/images/1/345/234/272/346/231/257.png +0 -0
  5. package/docs/images/1/345/234/272/346/231/257_en.png +0 -0
  6. package/docs/images/2MCP.png +0 -0
  7. package/docs/images/2MCP_en.png +0 -0
  8. package/docs/images/3/346/212/200/350/203/275.png +0 -0
  9. package/docs/images/3/346/212/200/350/203/275_en.png +0 -0
  10. package/docs/images/4/345/255/220/346/231/272/350/203/275/344/275/223.png +0 -0
  11. package/docs/images/4/345/255/220/346/231/272/350/203/275/344/275/223_en.png +0 -0
  12. package/docs/images/5/346/217/220/347/244/272/350/257/215.png +0 -0
  13. package/docs/images/5/346/217/220/347/244/272/350/257/215_en.png +0 -0
  14. package/docs/images/6/350/256/260/345/277/206.png +0 -0
  15. package/docs/images/6/350/256/260/345/277/206_en.png +0 -0
  16. package/docs/images/7/344/274/232/350/257/235.png +0 -0
  17. package/docs/images/7/344/274/232/350/257/235_en.png +0 -0
  18. package/docs/images/8/345/205/274/345/256/271.png +0 -0
  19. package/docs/images/8/345/205/274/345/256/271_en.png +0 -0
  20. package/docs/update.md +56 -38
  21. package/lib/agents-md/service.js +84 -5
  22. package/lib/client.js +1488 -475
  23. package/lib/compat/preset-reach.js +425 -0
  24. package/lib/compat/probe.js +8 -0
  25. package/lib/http-fence.js +21 -0
  26. package/lib/hub.js +160 -2
  27. package/lib/index.js +687 -47
  28. package/lib/mcp/override-blocks.js +195 -0
  29. package/lib/mcp/state-section.js +90 -0
  30. package/lib/prompt-sections.js +148 -0
  31. package/lib/rules/archive-engine.js +99 -29
  32. package/lib/rules/archive.js +56 -28
  33. package/lib/rules/service.js +332 -35
  34. package/lib/skills/core.js +15 -1
  35. package/lib/skills/service.js +3 -0
  36. package/lib/subagents/catalog.js +89 -0
  37. package/lib/subagents/service.js +384 -41
  38. package/lib/subagents/tools.js +18 -5
  39. package/package.json +97 -96
  40. package/screenshots.json +10 -10
  41. package/docs/Changelog.md +0 -144
  42. package/lib/rules/provider.js +0 -121
package/lib/client.js CHANGED
@@ -2,6 +2,14 @@
2
2
  // Registers the "MCP 管理" settings page. Talks to the host half through the
3
3
  // exact-path HTTP route /dsh-plugin-tool-management/api (same origin) instead of the
4
4
  // dynamic-only host.call channel.
5
+ //
6
+ // 取词三件套(design-plan D25)——改文案前先确认当前处落在哪一支:
7
+ // * `t` —— apply 开头「取词服务」段绑定的那份取词(导航标题等**注册期**取值也走它);
8
+ // 页面组件则经 props 拿到同一份(`pages.t`)。
9
+ // * `mt` —— MCP 页专用包装(定义在 apply 内):模块作用域的 `t` 会被宿主 locale 覆盖,
10
+ // 它每次取值都取最新的那个,所以 MCP 页一律用 `mt`。
11
+ // * `translateOrFallback(t, key, fallback)` —— 动态键(宿主回传的错误码等)缺失时的兜底文案。
12
+ // 三者共用同一份 DICT:键必须在 zh / en 两侧都存在(`npm run check:i18n` 会拦)。
5
13
  window.__ModuleLoader__.load({
6
14
  id: 'dsh-plugin-tool-management',
7
15
  factory: (require) => {
@@ -11,8 +19,8 @@ window.__ModuleLoader__.load({
11
19
  const React = require('react')
12
20
 
13
21
  const CSS =
14
- '.dsm-summary.dsm-summary-3{grid-template-columns:repeat(3,minmax(0,1fr))}' +
15
- '.dsm-version{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:500;letter-spacing:.3px}' +
22
+ '.dsm-stat-row{display:flex;min-width:0;align-items:center;gap:10px}.dsm-stat-row .dsm-summary{flex:1;min-width:0}' +
23
+ '.dsm-version{color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:500;letter-spacing:.3px;line-height:1}' +
16
24
  '.dsm-failed{color:var(--dsw-alias-state-error-primary);font-size:12px;white-space:nowrap}' +
17
25
  '.dsm-row-hint{grid-column:1/-1;margin:-4px 0 9px;color:#d49245;font-size:11px;line-height:16px}' +
18
26
  '.dsm-tools{display:flex;flex-direction:column;gap:8px}' +
@@ -32,14 +40,40 @@ window.__ModuleLoader__.load({
32
40
  '.dsm-detail-title-row{display:flex;align-items:center;justify-content:space-between;gap:8px}' +
33
41
  '.dsm-note-user{color:var(--dsw-alias-label-tertiary)}' +
34
42
  `
35
- .dsm-tabs{display:flex;gap:4px;border-bottom:1px solid var(--dsw-alias-border-l1);margin-bottom:14px}.dsm-tabs-end{display:flex;align-items:center;gap:4px;margin-left:auto}.dsm-tab{padding:8px 14px;border:0;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:13px;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px}.dsm-tab-active{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-success-primary)}.dsm-section{box-sizing:border-box;display:flex;width:100%;max-width:820px;min-width:0;margin:0 auto;padding:2px 0 36px;container-type:inline-size;flex-direction:column;gap:14px;color:var(--dsw-alias-label-primary);font-family:inherit}.dsm-head{display:flex;flex-direction:column;align-items:stretch;gap:16px}.dsm-title-block{min-width:0}.dsm-title-row{display:flex;align-items:center;gap:8px 12px;min-width:0;flex-wrap:wrap}.dsm-feedback-links{display:flex;align-items:center;gap:4px;flex-wrap:wrap}.dsm-title{margin:0;font-size:24px;line-height:32px;font-weight:600;letter-spacing:-.4px;white-space:nowrap}.dsm-feedback-link{display:inline-flex;min-height:28px;align-items:center;gap:5px;padding:0 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;font-weight:500;line-height:18px;text-decoration:none;white-space:nowrap}.dsm-feedback-link:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.dsm-feedback-link:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}.dsm-feedback-link svg{flex:none}.dsm-desc{margin:12px 0 0;color:var(--dsw-alias-label-tertiary);font-size:14px;line-height:22px}.dsm-actions{display:flex;flex-wrap:wrap;gap:8px;margin-left:0;flex:none}
36
- .dsm-btn{box-sizing:border-box;display:inline-flex;min-height:34px;align-items:center;justify-content:center;padding:0 13px;border:1px solid transparent;border-radius:8px;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);font:inherit;font-size:13px;font-weight:580;white-space:nowrap;cursor:pointer}.dsm-btn:hover:not(:disabled){filter:brightness(1.08)}.dsm-btn:disabled{opacity:.48;cursor:default}.dsm-btn-secondary,.dsm-btn-quiet{border-color:var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary)}.dsm-btn-quiet{min-height:28px;padding:0 9px;color:var(--dsw-alias-label-secondary);font-size:12px}.dsm-btn-danger{border-color:var(--dsw-alias-state-error-primary);background:transparent;color:var(--dsw-alias-state-error-primary)}.dsm-btn:focus-visible,.dsm-control:focus-visible,.dsm-select-trigger:focus-visible,.dsm-source-head:focus-visible,.dsm-switch:focus-visible,.dsm-upload-link:focus-visible,.dsm-file-remove:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}
37
- .dsm-summary{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}.dsm-stat{padding:12px 14px;border-right:1px solid var(--dsw-alias-border-l1);font-size:13px;color:var(--dsw-alias-label-secondary)}.dsm-stat:last-child{border-right:0}.dsm-stat strong{margin-right:5px;color:var(--dsw-alias-label-primary);font-size:17px;font-weight:680}.dsm-filters{display:flex;gap:9px}.dsm-search{flex:1}.dsm-source-filter{width:210px;flex:none}
43
+ .dsm-hint{display:flex;min-width:0;align-items:center;gap:5px;margin:0;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:18px}.dsm-hint-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dsm-hint-mark{display:inline-flex;width:14px;height:14px;flex:none;align-items:center;justify-content:center;border:1px solid var(--dsw-alias-border-l2);border-radius:50%;font-size:9px;line-height:1;cursor:help}.dsm-pick-list{display:flex;max-height:240px;flex-direction:column;overflow:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;padding:4px}.dsm-tabs-end{display:flex;align-items:center;gap:4px;margin-left:auto}.dsm-tab{padding:8px 14px;border:0;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:13px;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px}.dsm-tab-active{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-success-primary)}.dsm-section{box-sizing:border-box;display:flex;width:100%;max-width:820px;min-width:0;margin:0 auto;padding:2px 0 36px;container-type:inline-size;flex-direction:column;gap:14px;color:var(--dsw-alias-label-primary);font-family:inherit}.dsm-head{display:flex;flex-direction:column;align-items:stretch;gap:16px}.dsm-title-block{min-width:0}.dsm-title-row{display:flex;align-items:center;gap:8px 12px;min-width:0;flex-wrap:wrap}.dsm-feedback-links{display:flex;flex-direction:column;align-items:flex-end;gap:2px}.dsm-title{margin:0;font-size:24px;line-height:32px;font-weight:600;letter-spacing:-.4px;white-space:nowrap}.dsm-feedback-link{display:inline-flex;min-height:28px;align-items:center;gap:5px;padding:0 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;font-weight:500;line-height:18px;text-decoration:none;white-space:nowrap}.dsm-feedback-link:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.dsm-feedback-link:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}.dsm-feedback-link svg{flex:none}.dsm-desc{margin:12px 0 0;display:-webkit-box;overflow:hidden;color:var(--dsw-alias-label-tertiary);font-size:14px;line-height:22px;-webkit-box-orient:vertical;-webkit-line-clamp:2}.dsm-actions{display:flex;flex-wrap:wrap;gap:8px;margin-left:0;flex:none}
44
+ .dsm-btn{box-sizing:border-box;display:inline-flex;min-height:34px;align-items:center;justify-content:center;padding:0 13px;border:1px solid transparent;border-radius:8px;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);font:inherit;font-size:13px;font-weight:580;white-space:nowrap;cursor:pointer}.dsm-btn:hover:not(:disabled){filter:brightness(1.08)}.dsm-btn:disabled{opacity:.48;cursor:default}.dsm-btn-secondary{border-color:var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary)}.dsm-btn-quiet{border-color:var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);min-height:28px;padding:0 9px;font-size:12px}.dsm-btn-danger{border-color:var(--dsw-alias-state-error-primary);background:transparent;color:var(--dsw-alias-state-error-primary)}.dsm-btn-lock{border-color:#d49245;background:transparent;color:#d49245}.dsm-btn:focus-visible,.dsm-control:focus-visible,.dsm-select-trigger:focus-visible,.dsm-source-head:focus-visible,.dsm-switch:focus-visible,.dsm-upload-link:focus-visible,.dsm-file-remove:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}
45
+ .dsm-summary{display:grid;grid-template-columns:repeat(var(--dsm-stat-cols,3),minmax(0,1fr));overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}.dsm-stat{padding:12px 14px;border-right:1px solid var(--dsw-alias-border-l1);font-size:13px;color:var(--dsw-alias-label-secondary)}.dsm-stat:last-child{border-right:0}.dsm-stat strong,.dsm-stat-num{margin-right:5px;color:var(--dsw-alias-label-primary);font-size:17px;font-weight:680}.dsm-filters{display:flex;gap:9px}.dsm-search{flex:1}.dsm-source-filter{width:210px;flex:none}
46
+ .dsm-token-panel{display:flex;flex-direction:column;gap:8px}.dsm-token-row{display:flex;gap:8px;align-items:center;min-width:0}.dsm-token-row .dsm-control{flex:1;min-width:0}
38
47
  .dsm-control,.dsm-select-trigger{box-sizing:border-box;width:100%;min-height:34px;padding:0 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}.dsm-control::placeholder{color:var(--dsw-alias-label-tertiary)}textarea.dsm-control{min-height:160px;padding-top:9px;resize:vertical;line-height:20px}.dsm-select{position:relative}.dsm-select-trigger{display:flex;align-items:center;justify-content:space-between;text-align:left;cursor:pointer}.dsm-select-menu{position:absolute;z-index:40;top:calc(100% + 5px);right:0;left:0;display:flex;max-height:260px;padding:5px;overflow:auto;flex-direction:column;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-3);box-shadow:var(--dsw-shadow-lv2)}.dsm-option{padding:8px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;text-align:left;cursor:pointer}.dsm-option:hover,.dsm-option[aria-selected=true]{background:var(--dsw-alias-interactive-bg-hover)}
39
- .dsm-sources{display:flex;flex-direction:column;gap:9px}.dsm-source{overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}.dsm-source-head{box-sizing:border-box;display:flex;width:100%;min-height:48px;align-items:center;padding:0 13px}.dsm-source-head-main{display:flex;min-width:0;min-height:48px;flex:1;align-items:center;gap:10px;padding:0;border:0;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}.dsm-source-head:hover,.dsm-row:hover,.dsm-trash-row:hover,.dsm-hist-row:hover{background:var(--dsw-alias-interactive-bg-hover)}.dsm-source-title{font-size:14px;font-weight:650}.dsm-count{color:var(--dsw-alias-label-tertiary);font-size:12px}.dsm-path{min-width:0;margin-left:auto;overflow:hidden;color:var(--dsw-alias-label-tertiary);font-size:11px;text-overflow:ellipsis;white-space:nowrap}.dsm-source-actions{display:flex;align-items:center;gap:9px;margin-left:8px}.dsm-source-body{border-top:1px solid var(--dsw-alias-border-l1)}.dsm-source-note{padding:9px 13px;border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}
48
+ .dsm-sources{display:flex;flex-direction:column;gap:9px}.dsm-source{overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}.dsm-source-head{box-sizing:border-box;display:flex;width:100%;min-height:48px;align-items:center;padding:0 13px}.dsm-source-head-main{display:flex;min-width:0;min-height:48px;flex:1;align-items:center;gap:10px;padding:0;border:0;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}.dsm-source-head:hover,.dsm-row:hover,.dsm-trash-row:hover,.dsm-hist-row:hover{background:var(--dsw-alias-interactive-bg-hover)}.dsm-source-title{font-size:14px;font-weight:650;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dsm-count{color:var(--dsw-alias-label-tertiary);font-size:12px;flex:none}.dsm-path{min-width:0;margin-left:auto;overflow:hidden;color:var(--dsw-alias-label-tertiary);font-size:11px;text-overflow:ellipsis;white-space:nowrap}.dsm-source-actions{display:flex;align-items:center;gap:9px;margin-left:8px}.dsm-source-body{border-top:1px solid var(--dsw-alias-border-l1)}.dsm-source-note{padding:9px 13px;border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}
40
49
  .dsm-table-head,.dsm-row{display:grid;grid-template-columns:minmax(180px,1fr) 120px 90px max-content;align-items:center;column-gap:12px;padding:0 13px}.dsm-table-head{min-height:32px;border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:11px}.dsm-row{min-height:58px;border-bottom:1px solid var(--dsw-alias-border-l1)}.dsm-row:last-child{border-bottom:0}.dsm-main{min-width:0}.dsm-name{overflow:hidden;font-size:13px;font-weight:570;text-overflow:ellipsis;white-space:nowrap}.dsm-note{overflow:hidden;margin-top:2px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:17px;text-overflow:ellipsis;white-space:nowrap}.dsm-tags{display:flex;align-items:center;gap:5px;flex-wrap:wrap}.dsm-tag{display:inline-flex;min-height:19px;align-items:center;padding:0 6px;border:1px solid var(--dsw-alias-border-l3);border-radius:4px;color:var(--dsw-alias-label-secondary);font-size:10px;white-space:nowrap}.dsm-tag-on{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.dsm-tag-off{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.dsm-enabled{color:var(--dsw-alias-state-success-primary);font-size:12px;white-space:nowrap}.dsm-disabled{color:#d49245;font-size:12px;white-space:nowrap}.dsm-shadowed{color:var(--dsw-alias-label-tertiary);font-size:12px;white-space:nowrap}.dsm-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:7px}
41
50
  .dsm-switch{position:relative;width:34px;height:20px;flex:none;padding:0;border:0;border-radius:999px;background:var(--dsw-alias-border-l3);cursor:pointer}.dsm-switch:after{position:absolute;top:3px;left:3px;width:14px;height:14px;border-radius:50%;background:#fff;content:"";transition:transform 160ms ease}.dsm-switch-on{background:var(--dsw-alias-state-success-primary)}.dsm-switch-on:after{transform:translateX(14px)}.dsm-switch:disabled{opacity:.45;cursor:default}.dsm-trash-row{display:flex;min-height:48px;align-items:center;padding:0 13px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);color:inherit;font:inherit;font-size:13px;cursor:pointer}.dsm-trash-count{margin-left:auto;padding:2px 7px;border-radius:99px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);font-size:11px}.dsm-empty{padding:25px 14px;color:var(--dsw-alias-label-tertiary);font-size:12px;text-align:center}.dsm-feedback{padding:9px 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;color:var(--dsw-alias-label-secondary);font-size:12px}.dsm-warning{border-color:#d49245;color:#d49245}.dsm-error{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
42
- .dsm-mask{position:fixed;z-index:1100;inset:0;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(0,0,0,.62)}.dsm-modal{box-sizing:border-box;display:flex;width:min(560px,100%)!important;max-height:min(760px,calc(100vh - 48px));min-width:0;flex-direction:column;gap:16px;padding:22px;overflow:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-shadow-lv3)}.dsm-modal-wide{width:min(720px,100%)!important}.dsm-modal-import{width:min(480px,100%)!important;padding:24px}.dsm-modal-head{display:flex;align-items:flex-start;gap:12px}.dsm-modal-title{margin:0;flex:1;font-size:17px;line-height:24px;font-weight:670}.dsm-form,.dsm-field,.dsm-detail-section{display:flex;flex-direction:column}.dsm-form{gap:12px}.dsm-field{gap:6px}.dsm-label,.dsm-detail-title{font-size:12px}.dsm-label{color:var(--dsw-alias-label-secondary)}.dsm-detail-title{font-weight:650}.dsm-help{margin:0;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:18px}.dsm-modal-actions{display:flex;justify-content:flex-end;flex-wrap:wrap;gap:8px}.dsm-hidden-input{display:none}.dsm-dropzone{box-sizing:border-box;display:flex;width:100%;min-height:170px;flex-direction:column;align-items:center;justify-content:center;gap:7px;padding:22px;border:1px dashed var(--dsw-alias-border-l3);border-radius:12px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);transition:border-color 180ms ease,background 180ms ease;cursor:pointer}.dsm-dropzone:hover,.dsm-dropzone-active{border-color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-interactive-bg-hover)}.dsm-dropzone-title{color:var(--dsw-alias-label-primary);font-size:18px;font-weight:700}.dsm-dropzone-copy{font-size:12px;line-height:18px;text-align:center}.dsm-upload-choices{display:flex;align-items:center;gap:7px}.dsm-upload-link,.dsm-file-remove{padding:0;border:0;background:transparent;font:inherit;font-size:12px;cursor:pointer}.dsm-upload-link{color:var(--dsw-alias-label-secondary)}.dsm-upload-link:hover{color:var(--dsw-alias-label-primary);text-decoration:underline}.dsm-upload-link:disabled{opacity:.45;cursor:default}.dsm-upload-divider{color:var(--dsw-alias-label-tertiary);font-size:11px}.dsm-file{display:flex;align-items:center;gap:9px;padding:10px 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:12px}.dsm-file-kind{display:inline-flex;min-width:30px;height:24px;align-items:center;justify-content:center;border-radius:5px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);font-size:9px;font-weight:700}.dsm-file-name{min-width:0;overflow:hidden;flex:1;text-overflow:ellipsis;white-space:nowrap}.dsm-file-meta{color:var(--dsw-alias-label-tertiary);font-size:11px;white-space:nowrap}.dsm-file-remove{width:24px;height:24px;color:var(--dsw-alias-label-secondary);font-size:18px}.dsm-upload-requirements{padding:1px 1px 0}.dsm-upload-requirements ul{display:flex;margin:7px 0 0;padding-left:18px;flex-direction:column;gap:5px;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}.dsm-detail-section{gap:7px}.dsm-detail-path,.dsm-code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px}.dsm-detail-path{padding:8px 10px;border-radius:7px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);word-break:break-all}.dsm-code{max-height:280px;margin:0;padding:12px;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);line-height:18px;white-space:pre-wrap}.dsm-diag{padding:8px 10px;border-left:2px solid #d49245;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-size:12px}.dsm-trash-item{display:flex;align-items:center;gap:10px;padding:11px 0;border-bottom:1px solid var(--dsw-alias-border-l1)}.dsm-trash-item:last-child{border-bottom:0}.dsm-trash-main{min-width:0;flex:1}
51
+ .dsm-mask{position:fixed;z-index:1100;inset:0;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(0,0,0,.62)}.dsm-modal{box-sizing:border-box;display:flex;width:min(560px,100%)!important;max-height:min(760px,calc(100vh - 48px));min-width:0;flex-direction:column;gap:16px;padding:22px;overflow:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-shadow-lv3)}.dsm-modal-sm,.dsm-modal-import{width:min(480px,100%)!important}.dsm-modal-md{width:min(560px,100%)!important}.dsm-modal-lg,.dsm-modal-wide{width:min(720px,100%)!important}.dsm-modal-list{width:min(560px,100%)!important;height:min(640px,calc(100vh - 48px))!important;max-height:none!important;overflow:hidden}.dsm-modal-body{display:flex;min-height:0;flex-direction:column;gap:10px}.dsm-modal-list .dsm-modal-body{flex:1;overflow:auto;overscroll-behavior:contain}.dsm-modal-import{padding:24px}.dsm-modal-head{display:flex;align-items:flex-start;gap:12px}.dsm-modal-title{margin:0;flex:1;font-size:17px;line-height:24px;font-weight:670}.dsm-form,.dsm-field,.dsm-detail-section{display:flex;flex-direction:column}.dsm-form{gap:12px}.dsm-field{gap:6px}.dsm-label,.dsm-detail-title{font-size:12px}.dsm-label{color:var(--dsw-alias-label-secondary)}.dsm-detail-title{font-weight:650}.dsm-help{margin:0;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:18px}.dsm-modal-actions{display:flex;justify-content:flex-end;flex-wrap:wrap;gap:8px}.dsm-hidden-input{display:none}.dsm-dropzone{box-sizing:border-box;display:flex;width:100%;min-height:170px;flex-direction:column;align-items:center;justify-content:center;gap:7px;padding:22px;border:1px dashed var(--dsw-alias-border-l3);border-radius:12px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);transition:border-color 180ms ease,background 180ms ease;cursor:pointer}.dsm-dropzone:hover,.dsm-dropzone-active{border-color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-interactive-bg-hover)}.dsm-dropzone-title{color:var(--dsw-alias-label-primary);font-size:18px;font-weight:700}.dsm-dropzone-copy{font-size:12px;line-height:18px;text-align:center}.dsm-upload-choices{display:flex;align-items:center;gap:7px}.dsm-upload-link,.dsm-file-remove{padding:0;border:0;background:transparent;font:inherit;font-size:12px;cursor:pointer}.dsm-upload-link{color:var(--dsw-alias-label-secondary)}.dsm-upload-link:hover{color:var(--dsw-alias-label-primary);text-decoration:underline}.dsm-upload-link:disabled{opacity:.45;cursor:default}.dsm-upload-divider{color:var(--dsw-alias-label-tertiary);font-size:11px}.dsm-file{display:flex;align-items:center;gap:9px;padding:10px 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:12px}.dsm-file-kind{display:inline-flex;min-width:30px;height:24px;align-items:center;justify-content:center;border-radius:5px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);font-size:9px;font-weight:700}.dsm-file-name{min-width:0;overflow:hidden;flex:1;text-overflow:ellipsis;white-space:nowrap}.dsm-file-meta{color:var(--dsw-alias-label-tertiary);font-size:11px;white-space:nowrap}.dsm-file-remove{width:24px;height:24px;color:var(--dsw-alias-label-secondary);font-size:18px}.dsm-upload-requirements{padding:1px 1px 0}.dsm-upload-requirements ul{display:flex;margin:7px 0 0;padding-left:18px;flex-direction:column;gap:5px;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}.dsm-detail-section{gap:7px}.dsm-detail-path,.dsm-code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px}.dsm-detail-path{padding:8px 10px;border-radius:7px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);word-break:break-all}.dsm-code{max-height:280px;margin:0;padding:12px;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);line-height:18px;white-space:pre-wrap}.dsm-diag{padding:8px 10px;border-left:2px solid #d49245;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-size:12px}.dsm-trash-group-sub{margin:0;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
52
+ .dsm-trash-group{display:flex;flex-direction:column;overflow:hidden;margin-bottom:12px;border:1px solid var(--dsw-alias-border-l2);border-left-width:3px;border-radius:10px;background:var(--dsw-alias-bg-layer-2)}
53
+ /* 「这组动的是磁盘文件」用左侧色条区分:中性 = 文件不动;危险色 = 真的会删文件。 */
54
+ .dsm-trash-group-danger{border-left-color:var(--dsw-alias-state-error-primary)}
55
+ .dsm-trash-group-danger .dsm-trash-group-title{color:var(--dsw-alias-state-error-primary)}.dsm-trash-group:last-child{margin-bottom:0}.dsm-trash-group-head{position:sticky;top:0;z-index:1;display:flex;flex-direction:column;align-items:stretch;gap:2px;min-height:40px;justify-content:center;padding:8px 13px;border-bottom:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2)}
56
+ .dsm-trash-group-head-row{display:flex;min-width:0;align-items:center;gap:8px}.dsm-trash-group-title{font-size:13px;font-weight:650;color:var(--dsw-alias-label-primary)}.dsm-trash-group-body{padding:0 13px}
57
+ .dsm-trash-item{display:flex;align-items:center;gap:9px;padding:6px 10px;border-bottom:1px solid var(--dsw-alias-border-l1)}
58
+ .dsm-trash-item:last-child{border-bottom:0}
59
+ .dsm-trash-main{min-width:0;flex:1}
60
+ /* 回收站列表体:固定高度 + 上下滑动(记忆 / 技能 / 子智能体 / 场景 / 提示词 五处共用) */
61
+ .dsm-trash-box{max-height:min(46vh,340px);overflow-y:auto;overscroll-behavior:contain;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}
62
+
63
+ /* 滚动条:宿主默认是「悬停才出现」的浮层滚动条 —— 固定高度的列表弹窗里,用户看不出
64
+ 还能往下滚,等于下面的条目不存在。插件给自己的滚动容器画一条**常驻**细滚动条
65
+ (颜色走宿主变量,深浅主题都成立;不碰宿主其它地方的滚动条)。 */
66
+ .dsm-modal-list .dsm-modal-body,.dsm-trash-box,.dsm-pick-list,.dsm-seg-body,.dsm-dir-list,.dsm-trash-pane-body{scrollbar-width:thin;scrollbar-color:var(--dsw-alias-border-l2) transparent}
67
+ .dsm-modal-list .dsm-modal-body::-webkit-scrollbar,.dsm-trash-box::-webkit-scrollbar,.dsm-pick-list::-webkit-scrollbar,.dsm-seg-body::-webkit-scrollbar,.dsm-dir-list::-webkit-scrollbar,.dsm-trash-pane-body::-webkit-scrollbar{display:block;width:9px;height:9px}
68
+ .dsm-modal-list .dsm-modal-body::-webkit-scrollbar-track,.dsm-trash-box::-webkit-scrollbar-track,.dsm-pick-list::-webkit-scrollbar-track,.dsm-seg-body::-webkit-scrollbar-track,.dsm-dir-list::-webkit-scrollbar-track,.dsm-trash-pane-body::-webkit-scrollbar-track{background:var(--dsw-alias-bg-layer-1)}
69
+ .dsm-modal-list .dsm-modal-body::-webkit-scrollbar-thumb,.dsm-trash-box::-webkit-scrollbar-thumb,.dsm-pick-list::-webkit-scrollbar-thumb,.dsm-seg-body::-webkit-scrollbar-thumb,.dsm-dir-list::-webkit-scrollbar-thumb,.dsm-trash-pane-body::-webkit-scrollbar-thumb{border-radius:99px;background:var(--dsw-alias-border-l2)}
70
+ .dsm-modal-list .dsm-modal-body::-webkit-scrollbar-thumb:hover,.dsm-trash-box::-webkit-scrollbar-thumb:hover,.dsm-pick-list::-webkit-scrollbar-thumb:hover,.dsm-seg-body::-webkit-scrollbar-thumb:hover,.dsm-dir-list::-webkit-scrollbar-thumb:hover,.dsm-trash-pane-body::-webkit-scrollbar-thumb:hover{background:var(--dsw-alias-label-tertiary)}
71
+ .dsm-modal-list .dsm-modal-body::-webkit-scrollbar-corner,.dsm-trash-box::-webkit-scrollbar-corner,.dsm-pick-list::-webkit-scrollbar-corner,.dsm-seg-body::-webkit-scrollbar-corner,.dsm-dir-list::-webkit-scrollbar-corner,.dsm-trash-pane-body::-webkit-scrollbar-corner{background:transparent}
72
+
73
+ /* 列表弹窗的 body 是纵向 flex:直接子项一旦被 flex 收缩,而子项自己又是 overflow:hidden
74
+ (.dsm-trash-group 正是),超出的条目会被**直接裁掉** —— 看不见、也滚不到,表现就是
75
+ 「没有滚动条 + 内容缺一截」。列表项的尺寸一律由内容决定,滚动只交给 body 这一层。 */
76
+ .dsm-modal-list .dsm-modal-body>*{flex:none}
43
77
  .dsm-fm{display:flex;flex-direction:column;overflow:hidden;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}.dsm-fm-row{display:grid;grid-template-columns:minmax(96px,150px) minmax(0,1fr);gap:10px;padding:8px 12px;border-bottom:1px solid var(--dsw-alias-border-l1);font-size:12px;line-height:18px}.dsm-fm-row:last-child{border-bottom:0}.dsm-fm-key{color:var(--dsw-alias-label-secondary);word-break:break-word}.dsm-fm-raw{margin-left:5px;color:var(--dsw-alias-label-tertiary);font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px}.dsm-fm-val{min-width:0;color:var(--dsw-alias-label-primary);word-break:break-word;white-space:pre-wrap}
44
78
  @container(max-width:780px){.dsm-table-head{display:none}.dsm-row{grid-template-columns:minmax(0,1fr) max-content;gap:8px;padding:11px 13px}.dsm-row>.dsm-tags,.dsm-row>.dsm-status{grid-column:1}.dsm-row-actions{grid-column:2;grid-row:1 / span 3}.dsm-path{display:none}}@media(max-width:760px){.dsm-summary{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:720px){.dsm-title-row{flex-wrap:wrap}}@container(max-width:520px){.dsm-head{flex-direction:column}.dsm-actions{width:100%;margin-left:0}.dsm-actions .dsm-btn{flex:1}.dsm-filters{flex-direction:column}.dsm-source-filter{width:100%}.dsm-summary{grid-template-columns:1fr}}
45
79
  .dsm-hist-row{display:flex;align-items:center;gap:11px;padding:10px 13px;border-bottom:1px solid var(--dsw-alias-border-l1)}.dsm-hist-row:last-child{border-bottom:0}.dsm-hist-main{min-width:0;flex:1}.dsm-hist-title{overflow:hidden;font-size:13px;font-weight:580;text-overflow:ellipsis;white-space:nowrap}.dsm-hist-cwd{overflow:hidden;margin-top:2px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:17px;text-overflow:ellipsis;white-space:nowrap}.dsm-hist-cwd-missing{color:var(--dsw-alias-state-error-primary)}.dsm-hist-actions{display:flex;gap:6px;flex:none}.dsm-hist-check,.dsm-hist-group-check{flex:none;width:15px;height:15px;margin:0 4px 0 0;cursor:pointer;accent-color:var(--dsw-alias-state-success-primary)}.dsm-hist-group-head{gap:10px}.dsm-hist-group-extra{display:flex;align-items:center;gap:8px;flex:none;margin-left:4px}.dsm-hist-group-extra .dsm-tag{flex:none}.dsm-hist-register-path{margin-top:-4px;color:var(--dsw-alias-label-tertiary);font-family:var(--dsw-font-mono,monospace);font-size:11px;word-break:break-all}.dsm-hist-batch{display:flex;align-items:center;gap:8px;margin-left:2px;padding-left:10px;border-left:1px solid var(--dsw-alias-border-l2)}.dsm-hist-batch-count{color:var(--dsw-alias-label-secondary);font-size:12px;white-space:nowrap}.dsm-hist-batch-actions{display:flex;align-items:center;flex-wrap:wrap;gap:8px;flex:none}.dsm-dir-row{display:flex;gap:8px}.dsm-dir-row .dsm-control{flex:1}.dsm-dir-list{display:flex;max-height:220px;flex-direction:column;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}.dsm-dir-item{display:flex;width:100%;align-items:center;padding:8px 12px;border:0;border-bottom:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;text-align:left;cursor:pointer}.dsm-dir-item:last-child{border-bottom:0}.dsm-dir-item:hover{background:var(--dsw-alias-interactive-bg-hover)}.dsm-dir-item::before{content:"📁";margin-right:8px;font-size:12px}
@@ -124,6 +158,20 @@ window.__ModuleLoader__.load({
124
158
  .dsm-adv-caret{color:var(--dsw-alias-label-tertiary);font-size:10px}
125
159
  .dsm-adv-body{display:flex;flex-direction:column;gap:12px;padding:11px;border-top:1px solid var(--dsw-alias-border-l1)}
126
160
  .dsm-adv-note{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}
161
+ /* ── 工具限制四行(人设表单:一行一个 Agent 预设,白/黑名单互斥、默认折叠)──────── */
162
+ .dsm-modes{display:flex;flex-direction:column;gap:6px;margin-top:6px}
163
+ .dsm-mode-row{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}
164
+ .dsm-mode-row.dsm-mode-on{border-color:var(--dsw-alias-border-l3)}
165
+ .dsm-mode-head{display:flex;min-height:38px;align-items:center;gap:9px;padding:0 10px}
166
+ .dsm-mode-name{font-size:12px;font-weight:570;white-space:nowrap}
167
+ .dsm-mode-sum{color:var(--dsw-alias-label-tertiary);font-size:11px}
168
+ .dsm-mode-sum.dsm-mode-allow{color:var(--dsw-alias-state-success-primary)}
169
+ .dsm-mode-sum.dsm-mode-deny{color:#d49245}
170
+ .dsm-mode-actions{display:flex;align-items:center;gap:6px;margin-left:auto}
171
+ .dsm-mode-btn-allow{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}
172
+ .dsm-mode-btn-deny{border-color:#d49245;color:#d49245}
173
+ .dsm-mode-body{display:flex;flex-direction:column;gap:8px;padding:10px;border-top:1px solid var(--dsw-alias-border-l1)}
174
+ .dsm-legacy{display:flex;flex-direction:column;gap:6px;padding:9px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}
127
175
  /* ── 兼容页(宿主能力体检)──────────────────────────────────────────────
128
176
  一块"体检报告":先给结论卡,再给动作可用性、降级项、模块实体、阻塞项。
129
177
  状态色只用设计系统里已有的三档:成功 / 告警(#d49245) / 错误。 */
@@ -157,16 +205,44 @@ window.__ModuleLoader__.load({
157
205
  .dsm-compat-pill{display:inline-flex;min-height:20px;flex:none;align-items:center;padding:0 8px;border:1px solid var(--dsw-alias-border-l3);border-radius:999px;color:var(--dsw-alias-label-secondary);font-size:11px;white-space:nowrap}
158
206
  .dsm-compat-pill-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}
159
207
  .dsm-compat-pill-warn{border-color:#d49245;color:#d49245}
208
+ .dsm-compat-reach-marks{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:6px;min-width:0}
209
+ .dsm-reach-name{display:flex;flex-wrap:wrap;align-items:center;gap:6px}
210
+ .dsm-reach-chip{display:inline-flex;min-height:22px;align-items:center;padding:0 9px;border:1px solid var(--dsw-alias-border-l3);border-radius:6px;color:var(--dsw-alias-label-secondary);font-size:11px;white-space:nowrap}
211
+ .dsm-reach-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}
212
+ .dsm-reach-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
213
+ .dsm-reach-unknown{color:var(--dsw-alias-label-tertiary)}
160
214
  .dsm-compat-pill-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
161
215
  .dsm-compat-name{min-width:0;flex:1;font-size:12px;font-weight:560;word-break:break-word}
162
216
  .dsm-compat-label{margin-right:6px;color:var(--dsw-alias-label-tertiary);font-size:11px;white-space:nowrap}
163
217
  .dsm-compat-code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px}
164
218
  .dsm-compat-list{margin:0;padding-left:16px}.dsm-compat-list li+li{margin-top:4px}
165
219
  @container(max-width:640px){.dsm-compat-grid{grid-template-columns:1fr}}
166
- html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!important;scrollbar-gutter:stable}.dsm-tabs-shell{display:flex;min-width:0;align-items:center;gap:6px;margin-bottom:14px}.dsm-tabs{display:grid;min-width:0;flex:1;grid-template-columns:repeat(8,minmax(0,1fr));gap:0;overflow:hidden;margin-bottom:0}.dsm-tab{box-sizing:border-box;width:100%;min-width:0;padding-right:4px;padding-left:4px;overflow:hidden;font-size:12px;text-overflow:ellipsis;white-space:nowrap}.dsm-feedback-link{width:30px;height:30px;box-sizing:border-box;padding:0;justify-content:center}.dsm-feedback-link svg{width:16px;height:16px}@container(max-width:620px){.dsm-tab{padding-right:2px;padding-left:2px;font-size:11px}}
220
+ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!important;scrollbar-gutter:stable}.dsm-tabs-shell{position:sticky;top:0;z-index:6;display:flex;min-width:0;align-items:center;gap:6px;margin-bottom:14px;padding-top:2px;background:var(--dsw-alias-bg-layer-2)}.dsm-tabs{display:grid;min-width:0;flex:1;grid-template-columns:repeat(auto-fit,minmax(64px,1fr));gap:0;overflow:hidden;margin-bottom:0;border-bottom:1px solid var(--dsw-alias-border-l1)}.dsm-tab{box-sizing:border-box;width:100%;min-width:0;padding-right:4px;padding-left:4px;overflow:hidden;font-size:12px;text-overflow:ellipsis;white-space:nowrap}.dsm-feedback-link{width:30px;height:30px;box-sizing:border-box;padding:0;justify-content:center}.dsm-feedback-link svg{width:16px;height:16px}@container(max-width:620px){.dsm-tab{padding-right:2px;padding-left:2px;font-size:11px}}
167
221
  /* 动作可用性表:表头列宽与 dsm-compat-mod-row 一致;窄容器下隐藏表头。 */
168
222
  .dsm-compat-table-head{display:grid;grid-template-columns:minmax(0,1fr) max-content;gap:10px;padding:6px 12px;border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:11px}
169
223
  .dsm-compat-cards .dsm-compat-table-head{display:none}
224
+ /* 导出弹窗(D14):宽高都固定;只有「条目列表」这一层滚动,动作条常驻底部。 */
225
+ .dsm-export-modal .dsm-modal-body{display:flex;min-height:0;flex-direction:column;gap:14px;overflow:hidden}
226
+ .dsm-export-modal .dsm-export-form{display:flex;min-height:0;flex:1;flex-direction:column;gap:10px}
227
+ .dsm-export-modal .dsm-pick-list{flex:1;min-height:0;max-height:none;overflow:auto}
228
+ .dsm-export-modal .dsm-export-body{display:flex;min-height:0;flex:1;flex-direction:column;gap:6px}
229
+ .dsm-pick-group{display:flex;flex-direction:column}
230
+ .dsm-pick-group-head{box-sizing:border-box;display:flex;width:100%;min-width:0;align-items:center;gap:7px;padding:7px 2px;border:0;background:transparent;color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;text-align:left;cursor:pointer}
231
+ .dsm-pick-group-head:hover{color:var(--dsw-alias-label-primary)}
232
+ .dsm-pick-caret{flex:0 0 auto;width:12px;color:var(--dsw-alias-label-tertiary)}
233
+ .dsm-pick-group-name{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
234
+ .dsm-pick-group-count{flex:0 0 auto;color:var(--dsw-alias-label-tertiary);font-weight:500}
235
+ .dsm-pick-group .dsm-pick{margin-left:16px}
236
+ .dsm-pick-group .dsm-pick+.dsm-pick{margin-top:2px}
237
+
238
+ /* 技能回收站:上下两块各占一半(目录 / 技能),滚动条在每块列表区里面,大弹窗自己不滚。
239
+ 必须放在最后:盖过上面的 .dsm-modal-list .dsm-modal-body>*{flex:none},
240
+ 否则外层不撑满、两块就各按内容长,50/50 失效。宽度沿用 .dsm-modal-list 的 560px。 */
241
+ .dsm-modal-trash-split .dsm-modal-body{overflow:hidden}
242
+ .dsm-modal-trash-split .dsm-trash-split{flex:1 1 auto;min-height:0;display:grid;grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr) minmax(0,1fr);gap:12px}
243
+ .dsm-modal-trash-split .dsm-trash-group{margin-bottom:0;min-height:0}
244
+ .dsm-modal-trash-split .dsm-trash-pane-body{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:0 13px}
245
+
170
246
  `
171
247
 
172
248
  function ensureCss() {
@@ -194,6 +270,20 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
194
270
  }
195
271
  } catch (e) { /* storage unavailable */ }
196
272
 
273
+ /**
274
+ * 改令牌并落盘。明文机密 op(mcpm-reveal / mcpm-export)在宿主侧**必须**带对令牌,
275
+ * 没配令牌时一律拒绝 —— 所以界面得有个地方让用户把令牌填进来,否则那功能永远打不开。
276
+ * 令牌只存本机 localStorage,随请求以 x-dsh-token 发出。
277
+ */
278
+ function setAccessToken(value) {
279
+ TOKEN = String(value || '').trim()
280
+ try {
281
+ if (TOKEN) window.localStorage.setItem('dsh-plugin-tool-management-token', TOKEN)
282
+ else window.localStorage.removeItem('dsh-plugin-tool-management-token')
283
+ } catch (e) { /* storage unavailable */ }
284
+ return TOKEN
285
+ }
286
+
197
287
  function apiCall(op, args) {
198
288
  // `x-dsh-plugin` is the cross-site (CSRF) gate header the host half
199
289
  // requires on every request; a cross-origin page cannot attach it
@@ -247,6 +337,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
247
337
  function CompatPage(props) {
248
338
  const t = props.t
249
339
  const [data, setData] = React.useState(null)
340
+ const [presetReach, setPresetReach] = React.useState(null)
250
341
  const [busy, setBusy] = React.useState(false)
251
342
  const [error, setError] = React.useState(null)
252
343
  const apply = function (r, alive) {
@@ -254,12 +345,24 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
254
345
  if (r && r.ok) { setData(r); setError(null) } else { setError((r && r.error) || 'unknown') }
255
346
  setBusy(false)
256
347
  }
348
+ // 预设可达性是独立的一面镜子:它取不到只意味着这一节不显示,
349
+ // 绝不让它的失败顶掉 compat-status 的结论。
350
+ const applyReach = function (r, alive) {
351
+ if (alive === false) return
352
+ if (r && r.ok) setPresetReach(r)
353
+ }
354
+ const loadReach = function (alive) {
355
+ apiCall('preset-reach', {})
356
+ .then(function (r) { applyReach(r, alive) })
357
+ .catch(function () { /* 未挂载预设服务 → 不渲染这一节 */ })
358
+ }
257
359
  React.useEffect(function () {
258
360
  let alive = true
259
361
  setBusy(true)
260
362
  apiCall('compat-status', { refresh: true })
261
363
  .then(function (r) { apply(r, alive) })
262
364
  .catch(function (e) { if (alive) { setError(String((e && e.message) || e)); setBusy(false) } })
365
+ loadReach(alive)
263
366
  return function () { alive = false }
264
367
  }, [])
265
368
  const reload = function () {
@@ -267,6 +370,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
267
370
  apiCall('compat-status', { refresh: true })
268
371
  .then(function (r) { apply(r, true) })
269
372
  .catch(function (e) { setError(String((e && e.message) || e)); setBusy(false) })
373
+ loadReach(true)
270
374
  }
271
375
 
272
376
  const findings = (data && data.findings) || []
@@ -334,7 +438,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
334
438
 
335
439
  // 指标卡:宿主 / 本插件适配版本 / 可用能力(含插件自补的 optional 槽位)。
336
440
  push(React.createElement('div', { className: 'dsm-compat-grid' },
337
- card(t('compat.host'), (data.host && data.host.version) || '?', t('compat.peer') + ' ' + (data.expectedPeerRange || '-'), false),
441
+ card(t('compat.host'), (data.host && data.host.version) || '?', t('compat.peer') + ' ≥ ' + (data.minHostVersion || '-'), false),
338
442
  card(t('compat.verified'), data.verifiedVersion || '?', t('compat.verified.note'), false),
339
443
  card(t('compat.caps'), String(usableCount) + '/' + String(findings.length),
340
444
  blockedCount > 0
@@ -417,16 +521,51 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
417
521
  React.createElement('code', { className: 'dsm-compat-code' }, ' node scripts/doctor.mjs')))
418
522
  }
419
523
 
524
+ // 预设注入边界:五类能力(记忆 / 提示词 / 技能 / MCP / 子智能体)在**每个 Agent
525
+ // 预设**下到不到得了模型。回答的是"面板标着已注入,模型真的看得到吗"——官方
526
+ // minimal 预设的 persona 是 complete,会把除自己以外的提示词段全部压掉,而且它
527
+ // 连官方 subagent 工具都没挂。单独一块:取不到时这一节不显示,不影响上面的结论。
528
+ //
529
+ // 排版口径(用户裁定 2026-09-15):每格一枚描边标签,**绿框=能用、红框=不能用、
530
+ // 灰框=判断不了**;不再出现 ✓/✗ 符号,也不再有任何整行的解释文字 —— 预设损坏与
531
+ // 解析失败缩成预设名旁边的小标签。MCP 与子智能体两列由宿主侧的 preset-reach 给出。
532
+ if (presetReach && Array.isArray(presetReach.rows) && presetReach.rows.length) {
533
+ push(section(t('compat.reach'), t('compat.reach.hint'),
534
+ React.createElement('div', { className: 'dsm-compat-mod-list dsm-compat-cards' },
535
+ presetReach.rows.map(function (row) {
536
+ // ok → 绿框;absent / suppressed → 红框;其余(unknown,含宿主还没回传该字段)
537
+ // 一律灰框,绝不把"读不到"画成"不能用"。
538
+ const chip = function (label, state) {
539
+ const kind = state === 'ok' ? 'ok' : (state === 'absent' || state === 'suppressed') ? 'bad' : 'unknown'
540
+ return React.createElement('span', { className: 'dsm-reach-chip dsm-reach-' + kind, key: label }, label)
541
+ }
542
+ const tags = []
543
+ if (row.broken) tags.push(React.createElement('span', { className: 'dsm-tag dsm-tag-off', key: 'broken' }, t('compat.reach.broken')))
544
+ else if (row.reason) tags.push(React.createElement('span', { className: 'dsm-tag', key: 'reason' }, t('compat.reach.whyUnknown')))
545
+ return React.createElement('div', { className: 'dsm-compat-mod-row', key: row.presetId },
546
+ React.createElement('span', { className: 'dsm-compat-name dsm-reach-name' },
547
+ React.createElement('span', null, (row.name || row.presetId) + (row.isDefault ? ' · ' + t('compat.reach.default') : '')),
548
+ tags.length ? React.createElement('span', { className: 'dsm-tags' }, tags) : null),
549
+ React.createElement('span', { className: 'dsm-compat-reach-marks' },
550
+ chip(t('compat.reach.mark.memory'), row.memory),
551
+ chip(t('compat.reach.mark.agentsMd'), row.agentsMd),
552
+ chip(t('compat.reach.mark.skill'), row.skillCatalog),
553
+ chip(t('compat.reach.mark.mcp'), row.mcp),
554
+ chip(t('compat.reach.mark.subagent'), row.subagent)))
555
+ }))))
556
+
557
+ push(React.createElement('p', { className: 'dsm-help' }, t('compat.reach.tools')))
558
+ }
559
+
420
560
  return React.createElement('div', { className: 'dsm-compat' },
421
561
  React.createElement('div', { className: 'dsm-head' },
422
562
  React.createElement('div', { className: 'dsm-title-block' },
423
563
  React.createElement('div', { className: 'dsm-title-row' },
424
- React.createElement('h2', { className: 'dsm-title' }, t('compat.title')),
425
- VersionBadgeComponent ? React.createElement(VersionBadgeComponent, null) : null),
564
+ React.createElement('h2', { className: 'dsm-title' }, t('compat.title'))),
426
565
  React.createElement('p', { className: 'dsm-desc' }, t('compat.desc')))),
427
566
  React.createElement('div', { className: 'dsm-actions' },
428
567
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: reload },
429
- busy ? t('compat.checking') : t('compat.recheck'))),
568
+ busy ? t('btn.refreshing') : t('btn.refresh'))),
430
569
  body)
431
570
  }
432
571
 
@@ -440,13 +579,18 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
440
579
  // 契约测试则直接从导出里读它(apply 提前返回也必须拿得到)。
441
580
  const DICT = {
442
581
  zh: {
443
- "title": "技能", "desc": "管理本机技能:启停、导入、创建与回收站;同名技能可指定哪个版本生效。", "link.project": "GitHub", "link.feedback": "问题反馈",
444
- "btn.create": "创建技能", "btn.import": "导入", "btn.refresh": "刷新", "btn.cancel": "取消", "btn.close": "关闭", "btn.detail": "查看详情", "btn.trash": "移到回收站", "btn.restore": "恢复", "btn.delete.forever": "永久删除", "btn.file.pick": "选择文件", "btn.folder.pick": "选择文件夹", "btn.import.now": "安装", "btn.create.now": "创建技能", "btn.disable": "停用", "btn.enable": "启用", "btn.open.editor": "用系统编辑器打开", "btn.activate": "启用这个", "btn.activate.title": "把当前来源的版本设为同名技能的首选并启用(不改动任何源文件)", "btn.unprefer": "取消首选", "btn.unprefer.title": "取消同名首选,回到按来源优先级自动选择",
582
+ "title": "技能", "desc": "管理技能:启停、导入、创建、回收站。", "link.project": "GitHub", "link.feedback": "问题反馈",
583
+ "btn.create": "新增技能", "btn.import": "导入技能", "btn.refresh": "刷新", "btn.refreshing": "刷新中…", "btn.cancel": "取消", "btn.close": "关闭", "export.hint": "导出为 zip,写入你指定的目录;只读源文件,不改动任何数据。", "export.outDir": "导出目录", "export.outDir.placeholder": "例如 D:////backup", "export.pickDir": "选择导出目录", "export.submit": "导出", "export.busy": "导出中…", "export.done": "已导出 {count} 个文件到 {path}", "scenes.lock.lock": "锁定", "scenes.lock.unlock": "解锁", "scenes.lock.tag": "已锁定", "scenes.lock.hint": "锁定后五个管理域(MCP / 技能 / 子智能体 / 记忆 / 提示词)整体只读;场景启停不受影响", "scenes.lock.blockedEdit": "场景已锁定:先解锁再修改", "scenes.lock.notActive": "场景未启动:先启动再锁定", "scenes.lock.blockedExit": "场景已锁定:先解锁再关闭", "lock.banner": "有场景处于锁定状态:MCP / 技能 / 子智能体 / 记忆 / 提示词已整体冻结,先到场景页解锁再修改。", "export.empty": "没有可导出的条目", "export.pickAll": "全选", "export.pickNone": "全不选", "export.group.count": "{count} 项", "export.missing": ";跳过 {count} 项(文件不存在):{names}", "export.skills": "导出技能", "export.subagents": "导出子智能体", "export.presets": "导出提示词", "export.memories": "导出记忆", "btn.detail": "查看详情", "btn.trash": "移到回收站", "btn.restore": "恢复", "btn.delete.forever": "永久删除", "btn.file.pick": "选择文件", "btn.folder.pick": "选择文件夹", "btn.create.now": "创建技能", "btn.disable": "停用", "btn.enable": "启用", "btn.open.editor": "用系统编辑器打开", "btn.activate": "启用这个", "btn.activate.title": "把当前来源的版本设为同名技能的首选并启用(不改动任何源文件)", "btn.unprefer": "取消首选", "btn.unprefer.title": "取消同名首选,回到按来源优先级自动选择",
445
584
  "btn.custom.add": "添加目录", "btn.custom.remove": "移除", "btn.openDir": "选择",
446
- "btn.source.remove": "移除来源", "btn.source.remove.title": "不再读取这个来源(源文件不动,可恢复)", "btn.source.restore": "恢复读取",
447
- "removed.title": "已移除的来源", "removed.count": "{count} 个", "removed.hint": "插件不再读取这些目录;源文件与技能都还在磁盘上,点「恢复读取」即可重新纳入",
585
+ "btn.source.remove": "移除来源", "btn.source.remove.title": "不再读取这个来源(源文件不动,之后可在回收站里恢复)", "btn.source.restore": "恢复", "btn.source.forget": "永久删除",
586
+ "trash.section.dirs": "目录", "trash.section.skills": "技能", "trash.section.dirs.sub": "只是不再读取,磁盘文件一个字节都不动", "trash.section.dirs.empty": "没有被移除的来源", "trash.section.skills.sub": "文件真的被移走了:恢复能还原,永久删除会删掉文件", "trash.section.presets.sub": "预设目录已移走:恢复能还原,永久删除会删掉文件", "trash.section.scenes.sub": "场景记录、档案与它的记忆都在这里:恢复会整条放回", "trash.section.agents.sub": "人设文件已移走:恢复能还原,永久删除会删掉文件",
587
+ "trash.dirs.count.one": "{count} 个待处理目录", "trash.dirs.count.other": "{count} 个待处理目录",
588
+ "trash.row.summary": "技能 {skills} · 目录 {dirs}",
589
+ "confirm.source.forget.title": "永久删除这条来源记录?", "confirm.source.forget.desc": "「{name}」将从插件里彻底删掉:来源列表里不再有它,「恢复读取」也找不回来(需要时只能重新添加这个目录)。",
590
+ "confirm.source.forget.hint": "磁盘上的文件夹一个字节都不动:{path}。",
591
+ "result.sourceForgotten": "已永久删除来源记录:{name}",
448
592
  "confirm.source.remove.title": "移除来源?", "confirm.source.remove.desc": "「{name}」将不再被读取:它下面的技能不再出现在本页,也不再参与同名优先级;已启用/停用的选择会保留。",
449
- "confirm.source.remove.hint": "不删除任何文件。想恢复时点这一组的「恢复读取」。",
593
+ "confirm.source.remove.hint": "不删除任何文件。移除后它出现在「回收站 → 目录」,在那里点「恢复读取」就能重新纳入(也可以永久删除这条记录)。",
450
594
  "result.sourceRemoved": "已移除来源:{name}(不再读取)", "result.sourceRestored": "已恢复读取:{name}",
451
595
  "custom.add.title": "添加自定义技能目录", "custom.add.submit": "添加",
452
596
  "custom.add.path": "目录绝对路径", "custom.add.path.placeholder": "例如 D:\\skills\\my-skills",
@@ -457,14 +601,17 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
457
601
  "confirm.custom.remove.desc": "移除后该目录的技能不再列出(文件不受影响):{name}",
458
602
  "result.custom.added": "已添加自定义目录:{path}", "result.custom.removed": "已移除自定义目录:{name}",
459
603
  "status.enabled": "已启用", "status.disabled": "已停用", "status.invalid": "诊断异常", "status.shadowed": "被覆盖", "status.shadowed.hint": "同名技能「{name}」正在生效;点「启用这个」可改用当前来源的版本", "status.preferred": "同名首选", "status.readonly": "源文件只读", "status.notDeletable": "不可删除", "status.notDeletable.hint": "删除只对插件可管理的来源开放(DSH 技能 / 导入技能 / 项目级 .dsh/skills);外部 Agent 与自定义目录只读,里面的技能不能删", "status.manageable": "可管理", "status.project": "项目级", "status.rank": "优先级 {rank}", "status.source.on": "已启用", "status.source.off": "已停用", "status.bundle": "目录技能", "status.single": "单文件",
460
- "summary.total.one": "{count} 个技能", "summary.total.other": "{count} 个技能", "summary.enabled.one": "{count} 个已启用", "summary.enabled.other": "{count} 个已启用", "summary.disabled.one": "{count} 个已停用", "summary.disabled.other": "{count} 个已停用", "summary.issues.one": "{count} 个诊断项", "summary.issues.other": "{count} 个诊断项", "summary.group.one": "{count} 个技能", "summary.group.other": "{count} 个技能", "table.skill": "技能名称与描述", "table.status": "调用状态",
604
+ "summary.total.one": "{count} 个技能", "summary.total.other": "{count} 个技能", "summary.enabled.one": "{count} 个已启用", "summary.enabled.other": "{count} 个已启用", "summary.issues.one": "{count} 个诊断项", "summary.issues.other": "{count} 个诊断项", "summary.group.one": "{count} 个技能", "summary.group.other": "{count} 个技能", "table.skill": "技能名称与描述", "table.status": "调用状态",
461
605
  "filter.source": "来源", "filter.all": "全部来源", "filter.option": "{name}({count})", "search": "搜索", "search.placeholder": "搜索技能名称或描述", "search.clear": "清除搜索",
462
606
  "empty.search": "没有匹配的技能", "empty.source": "该来源不存在或暂无技能", "loading": "正在加载技能…", "note.missing": "未提供简介", "source.toggle": "启停来源", "skill.toggle": "启停技能",
463
607
  "source.external.note": "只读接入,启停不改写源文件", "source.dsh.note": "可创建、导入、移到回收站;默认来源必须读取,不能移除或停用",
464
608
  "detail.title": "技能详情", "detail.body": "正文", "detail.frontmatter": "元数据", "detail.noFrontmatter": "该技能未提供元数据。", "detail.diagnostics": "诊断", "detail.path": "源文件", "detail.noIssues": "未发现诊断问题。",
465
609
  "create.title": "创建技能", "create.target": "创建位置", "create.name": "名称", "create.name.placeholder": "例如 code-review-helper", "create.description": "简介", "create.description.placeholder": "一句话说明什么时候使用", "create.body": "正文(Markdown)", "create.body.placeholder": "写下技能要遵循的指令、步骤和边界…", "create.chat.note": "对话里 create_skill 建的是用户级技能",
466
- "import.title": "导入技能", "upload.drop.title": "点击或拖入此处", "upload.drop.copy": "支持 .zip、技能文件夹或单个 SKILL.md", "upload.selected.one": "{count} 个文件 · {size}", "upload.selected.other": "{count} 个文件 · {size}", "upload.remove": "移除所选内容", "upload.requirements": "文件要求", "upload.requirement.skill": "压缩包或文件夹需包含 SKILL.md", "upload.requirement.frontmatter": "SKILL.md 需包含 YAML 格式的技能名称和描述", "upload.requirement.copy": "导入时复制完整内容,不修改原始来源", "upload.importing": "正在安装…", "status.selected": "已选择", "select.file.invalid": "请选择 .zip 或单个 SKILL.md", "select.folder.invalid": "该文件夹里没有 SKILL.md", "error.browse.absolute": "目录路径必须是绝对路径:{path}", "error.browse.unreadable": "无法读取目录:{path}", "error.browse.notDirectory": "不是目录:{path}",
610
+ "import.title": "导入技能", "upload.drop.title": "点击或拖入此处", "upload.drop.copy": "支持 .zip、技能文件夹或单个 SKILL.md", "upload.selected.one": "{count} 个文件 · {size}", "upload.selected.other": "{count} 个文件 · {size}", "upload.remove": "移除所选内容", "upload.requirements": "文件要求", "upload.requirement.skill": "压缩包或文件夹需包含 SKILL.md", "upload.requirement.frontmatter": "SKILL.md 需包含 YAML 格式的技能名称和描述", "upload.requirement.copy": "导入时复制完整内容,不修改原始来源", "upload.requirement.persona.1": "支持 .md / .zip(可多选、可拖入)", "upload.requirement.persona.2": "一个 .md = 一个人设,文件名即人设名", "upload.requirement.persona.3": "同名自动跳过,绝不覆盖原有文件", "upload.requirement.memory.1": "支持 .md / .zip(可多选、可拖入)", "upload.requirement.memory.2": "一个 .md = 一条记忆,文件名即记忆名;带 SKILL.md 的目录 = bundle 记忆(附件一起进来)", "upload.requirement.memory.3": "zip 里的目录名即场景;没有目录时用下面「导入到场景」的值(留空 = 全局)", "upload.requirement.prompt.1": "支持 .md(可多选、可拖入)", "upload.requirement.prompt.2": "一个 .md = 一份预设,文件名即 id(目录名)", "upload.requirement.prompt.3": "同名已存在则跳过,绝不覆盖", "upload.requirement.session.1": "支持 .jsonl / .json / .md / .txt(一次一个)", "upload.requirement.session.2": "Claude Code / Cursor / Codex 的转录文件,或任意文本", "upload.requirement.session.3": "项目目录可留空 = 归入未分组;留空不影响导入", "upload.importing": "正在导入…", "status.selected": "已选择", "select.file.invalid": "请选择 .zip 或单个 SKILL.md", "select.folder.invalid": "该文件夹里没有 SKILL.md", "error.browse.absolute": "目录路径必须是绝对路径:{path}", "error.browse.unreadable": "无法读取目录:{path}", "error.browse.notDirectory": "不是目录:{path}",
467
611
  "trash.title": "回收站", "trash.count.one": "{count} 个待处理技能", "trash.count.other": "{count} 个待处理技能", "trash.empty": "回收站为空", "trash.deletedAt": "删除于 {time}", "trash.source": "来源:{source}",
612
+ "trash.btn.open": "回收站", "trash.items.count": "{count} 项", "trash.purge.confirm": "永久删除?",
613
+ "trash.group.agents": "人设", "trash.group.scenes": "场景", "trash.group.presets": "提示词预设",
614
+ "trash.restored": "已恢复「{name}」", "trash.purged": "已永久删除「{name}」", "trash.loadFailed": "读取回收站失败",
468
615
  "confirm.trash.title": "移到回收站?", "confirm.trash.desc": "「{name}」将移入回收站,可恢复", "confirm.delete.title": "永久删除?", "confirm.delete.desc": "「{name}」将永久删除,无法恢复",
469
616
  "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}",
470
617
  "warning.scan.truncated": "技能目录较大或嵌套过深,部分技能未显示:{path}", "warning.state.invalid": "状态文件不可读,已安全停用所有技能:{path}", "warning.backupUncleaned": "旧版本备份未清理:{path}({error})", "warning.project.unavailable": "无法从宿主读取活动工作区,项目技能未显示:{path}",
@@ -478,13 +625,13 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
478
625
  "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})",
479
626
  "diagnostic.frontmatter.missing": "缺少完整 YAML frontmatter", "diagnostic.name.missing": "frontmatter 缺少 name", "diagnostic.name.invalid": "技能名需 kebab-case:{name}", "diagnostic.description.missing": "frontmatter 缺少 description", "diagnostic.invocation.invalid": "调用策略字段值无效", "diagnostic.shadowed": "被更高优先级来源 {root} 覆盖",
480
627
  "action.enable": "启用", "action.disable": "停用", "action.create": "创建", "action.delete": "删除", "action.restore": "恢复", "action.toggle": "启用或停用",
481
- "mcp.desc": "管理本机 MCP 服务:新增、启停、重启与工具级开关。",
482
- "mcp.level.all": "全部级别", "mcp.level.project": "Profile 级", "mcp.level.global": "全局", "mcp.level.loader": "已加载",
628
+ "mcp.desc": "管理 MCP:新增、启停、重启,以及工具级开关。",
629
+ "mcp.level.all": "全部级别", "mcp.level.project": "应用级", "mcp.level.global": "全局", "mcp.level.loader": "已加载",
483
630
  "mcp.stat.total": "个服务", "mcp.stat.enabled": "个已启用", "mcp.stat.tools": "个工具",
484
631
  "mcp.search": "搜索服务", "mcp.search.placeholder": "搜索服务名称、地址或命令",
485
- "mcp.btn.new": "新增服务", "mcp.btn.add": "添加", "mcp.btn.save": "保存", "mcp.btn.detail": "详情", "mcp.btn.edit": "编辑", "mcp.btn.restart": "重启", "mcp.btn.remove": "删除",
632
+ "mcp.btn.new": "新增 MCP", "mcp.btn.add": "添加", "mcp.btn.save": "保存", "mcp.btn.detail": "详情", "mcp.btn.edit": "编辑", "mcp.btn.restart": "重启", "mcp.btn.remove": "删除",
486
633
  "mcp.btn.enableAll": "全部启用", "mcp.btn.disableAll": "全部停用",
487
- "mcp.loading": "正在加载 MCP 服务…", "mcp.empty": "暂无 MCP 服务,点「新增服务」添加", "mcp.empty.search": "没有匹配的服务。",
634
+ "mcp.loading": "正在加载 MCP 服务…", "mcp.empty": "暂无 MCP,点「新增 MCP」添加", "mcp.empty.search": "没有匹配的服务。",
488
635
  "mcp.servers.count": "{count} 个服务", "mcp.tools.count": "{count} 个工具", "mcp.duplicate": "重复 id",
489
636
  "mcp.table.name": "服务名称与地址", "mcp.table.transport": "传输与工具", "mcp.table.status": "运行状态",
490
637
  "mcp.live.notLoaded": "未加载", "mcp.live.failed": "启动失败", "mcp.live.stopped": "未运行", "mcp.live.loading": "加载中", "mcp.live.noTools": "无工具", "mcp.live.running": "运行中",
@@ -492,17 +639,22 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
492
639
  "mcp.note.prefix": "备注:", "mcp.toggleServer": "启停服务", "mcp.toggleTool": "启停工具",
493
640
  "mcp.msg.ok": "操作成功", "mcp.msg.failed": "操作失败", "mcp.msg.loadFailed": "加载失败", "mcp.msg.warn": "操作完成,但加载器有提示:{warning}",
494
641
  "mcp.msg.toolOn": "已启用工具:{name}", "mcp.msg.toolOff": "已停用工具:{name}",
495
- "mcp.form.addTitle": "新增 MCP 服务", "mcp.form.editTitle": "编辑 MCP 服务:",
642
+ "mcp.form.addTitle": "新增 MCP", "mcp.form.editTitle": "编辑 MCP 服务:",
496
643
  "mcp.field.serverName": "服务名称 serverName", "mcp.field.serverName.hint": "1-32 位 [A-Za-z0-9_-],补丁里按此名注册",
497
644
  "mcp.field.transport": "传输方式", "mcp.field.level": "级别",
498
- "mcp.field.level.project": "Profile 级(本应用:{path})", "mcp.field.level.global": "全局(跨 Profile:{path})",
645
+ "mcp.field.level.project": "应用级(仅当前应用:{path})", "mcp.field.level.global": "全局(跨应用:{path})",
499
646
  "mcp.field.url": "服务 URL", "mcp.field.url.hint": "需以 http(s):// 开头",
500
647
  "mcp.field.command": "启动命令", "mcp.field.args": "参数(空格或换行分隔)",
501
648
  "mcp.field.env": "环境变量(每行 key=value)", "mcp.field.env.hint": "路径按系统路径写法填(Windows 用 \\,macOS/Linux 用 /)。",
502
649
  "mcp.field.headers": "请求头(每行 key=value)",
503
650
  "mcp.detail.title": "服务详情:", "mcp.detail.config": "配置", "mcp.detail.showSecret": "显示密钥", "mcp.detail.hideSecret": "隐藏密钥",
651
+ "mcp.token.label": "访问令牌", "mcp.token.placeholder": "与宿主配置的 token 相同", "mcp.token.save": "保存并重试",
652
+ "mcp.token.hint": "明文密钥必须带对的访问令牌:宿主配置里设 token(或环境变量 DSH_PLUGIN_TOOL_MANAGEMENT_TOKEN)并重启 DSH,这里填同一个值。",
653
+ "mcp.btn.compact": "整理补丁", "mcp.btn.compact.title": "清掉补丁文件里已经不起作用的旧开关记录(每台服务的启用/停用状态不变)",
654
+ "mcp.compact.title": "整理补丁文件", "mcp.compact.desc": "配置里会积累一些已经不起作用的旧开关记录(同一个服务被反复启用/停用留下的)。",
655
+ "mcp.compact.confirm": "确认整理", "mcp.compact.done": "补丁文件已整理:清掉 {count} 条失效的旧开关记录,服务的启用/停用状态未变", "mcp.compact.clean": "补丁文件已经很干净,没有可清理的旧记录",
504
656
  "mcp.detail.entryId": "条目 ID", "mcp.detail.status": "运行状态", "mcp.detail.registered": "该服务已登记在 Loader 中。",
505
- "mcp.detail.note": "备注(仅本机可见)", "mcp.detail.note.placeholder": "例如:A 不可用时改用 B 兜底", "mcp.detail.note.save": "保存备注",
657
+ "mcp.detail.note": "备注(会注入系统提示词,随请求发送给模型)", "mcp.detail.note.placeholder": "例如:A 不可用时改用 B 兜底", "mcp.detail.note.save": "保存备注",
506
658
  "mcp.detail.tools": "工具({count})",
507
659
  "mcp.field.headersShort": "请求头", "mcp.field.envShort": "环境变量",
508
660
  "mcp.tools.loading": "正在获取工具列表…", "mcp.tools.loadFailed": "加载工具失败:", "mcp.tools.none": "该服务暂无已注册工具。",
@@ -512,11 +664,11 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
512
664
  "root.dsh": "DSH 技能", "root.hub": "导入技能", "root.agents": "公共 Agent", "root.ccswitch": "CC Switch", "root.projectDsh": "项目 DSH", "root.projectAgents": "项目 Agent", "root.codex": "Codex", "root.claude": "Claude", "root.gemini": "Gemini", "root.opencode": "OpenCode", "root.cursor": "Cursor",
513
665
  "memory.title": "记忆",
514
666
  "tabs.scenes": "场景", "tabs.skills": "技能", "tabs.subagents": "子智能体", "tabs.prompts": "提示词", "tabs.memory": "记忆", "tabs.sessions": "会话", "tabs.compat": "兼容",
515
- "compat.title": "宿主兼容", "compat.desc": "本插件与宿主 DSH 的对接体检:加载的是不是同一份模块、每个动作走哪条路径、哪些能力已降级。只读检查,不改动任何数据。",
667
+ "compat.title": "宿主兼容", "compat.desc": "本插件与 DSH 的对接体检(只读)。",
516
668
  "compat.state.ok": "一切正常", "compat.state.degraded": "有降级项", "compat.state.blocked": "有需要处理的问题",
517
669
  "compat.checking": "检查中…", "compat.recheck": "重新检查", "compat.empty": "尚未检查", "compat.failed": "检查失败",
518
670
  "compat.summary.ok": "宿主 {version} · 能力 {ok}/{total} · 全部可用", "compat.summary.degraded": "宿主 {version} · 能力 {ok}/{total} · 降级 {count} 项",
519
- "compat.host": "宿主版本", "compat.peer": "要求范围", "compat.verified": "插件适配版本", "compat.verified.note": "本插件按此版本验证",
671
+ "compat.host": "宿主版本", "compat.peer": "最低要求", "compat.verified": "插件适配版本", "compat.verified.note": "本插件按此版本验证",
520
672
  "compat.caps": "可用能力", "compat.caps.all": "全部通过", "compat.caps.degraded": "{count} 项不可用",
521
673
  "compat.caps.substituted": "缺口由插件自补",
522
674
  "compat.ops": "宿主动作可用性", "compat.ops.hint": "每个动作走哪条路径", "compat.ops.disabled": "有 {count} 个动作当前不可用(相关按钮已禁用并会说明原因)",
@@ -530,36 +682,41 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
530
682
  "compat.fallback.native": "改走原生入口", "compat.fallback.blocked": "相关按钮已禁用",
531
683
  "compat.modules": "模块实体(插件 vs 宿主)", "compat.modules.hint": "同一份模块才谈得上适配", "compat.module.same": "同一份模块", "compat.module.separate": "两份拷贝(需修复)", "compat.module.unknown": "无法比较",
532
684
  "compat.blockers": "阻塞项(按此修复)",
685
+ "compat.reach": "预设注入边界", "compat.reach.hint": "面板说已注入,模型真的看得到吗",
686
+ "compat.reach.mark.memory": "记忆", "compat.reach.mark.agentsMd": "提示词", "compat.reach.mark.skill": "技能", "compat.reach.mark.mcp": "MCP", "compat.reach.mark.subagent": "子智能体",
687
+ "compat.reach.default": "默认",
688
+ "compat.reach.whyUnknown": "判断不了", "compat.reach.broken": "预设损坏",
689
+ "compat.reach.tools": "本插件的模型工具(含 subagent_list / subagent_run)与 MCP 都挂在宿主平面,不受预设影响,任何预设下都可调用。",
533
690
  "compat.hint.doctor": "命令行体检:",
534
- "scenes.title": "场景", "scenes.desc": "管理场景:可以预设不同的使用场景,包含 MCP、Skills、子智能体、记忆",
691
+ "scenes.title": "场景", "scenes.desc": "管理场景:预设 MCP、技能、子智能体与记忆的组合,一键切换。",
535
692
  "scenes.stat.total": "个场景", "scenes.stat.active": "个已启用", "scenes.stat.archives": "个有档案",
536
693
  "scenes.noDesc": "还没有描述",
537
694
  "scenes.mode.active": "当前模式:{name}", "scenes.mode.profile": "档案:{parts}", "scenes.mode.noProfile": "这个场景还没有档案",
538
- "scenes.mode.enter": "切入此模式", "scenes.empty": "还没有专门设置的场景;全局记忆不需要场景就能注入,需要切换 MCP / 技能 / 人设时再新建。",
695
+ "scenes.empty": "还没有专门设置的场景;全局记忆不需要场景就能注入,需要切换 MCP / 技能 / 人设时再新建。",
539
696
  "scenes.field.desc.limit": "最多 {count} 字;超出部分在卡片上省略。",
540
697
  "scenes.profile.mcp": "MCP {count} 台", "scenes.profile.skills": "技能 {count} 个", "scenes.profile.subagents": "子智能体 {count} 个", "scenes.profile.memories": "记忆 {count} 条",
541
698
  "scenes.mcp.hint": "添加「MCP 工具集」后,勾选要启用的服务器(含未运行的);「选工具」可细化到具体工具。",
542
699
  "scenes.seg.selectAll": "全选", "scenes.seg.clear": "清空", "scenes.seg.checked": "{checked}/{total} 已勾选",
543
700
  "scenes.archive.sectionOff": "未定义",
544
- "scenes.archive.summary": "档案:MCP {mcp} 台 · 技能 {skills} 个 · 子智能体 {subagents} 个 · 记忆 {memories} 条",
545
- "scenes.archive.note": "新添加的段默认一项不勾。段未定义 = 不改动该域;段已定义但一项不勾 = 该域全部停用(子智能体段例外:不勾 = 不限制;记忆段只影响注入,不会删文件)。",
701
+ "scenes.archive.summary": "档案:MCP {mcp} 台 · 技能 {skills} 个 · 子智能体 {subagents} 个",
702
+ "scenes.archive.note": "段未定义 = 不改动该域;段已定义 = 按勾选集双向切换:勾的启用、没勾的停用。MCP 会真的启停服务器进程,技能会启停它所在的来源。",
546
703
  "scenes.mcp.noServers": "还没有可选的 MCP 服务器", "scenes.mcp.toolCount": "{count} 个工具", "scenes.skills.empty": "还没有可选的技能",
547
704
  "scenes.mcp.allTools": "全部工具", "scenes.mcp.pickedCount": "指定 {count} 个工具", "scenes.mcp.notRunning": "未运行", "scenes.mcp.pickTools": "选工具",
548
705
  "scenes.mcp.toolsOf": "工具明细", "scenes.mcp.back": "返回", "scenes.mcp.noTools": "该服务器当前没有可列出的工具(未运行或无工具)", "scenes.mcp.drillHint": "勾选 = 该场景下启用;不勾 = 停用。整台勾选时默认全部工具。",
549
706
  "scenes.skills.hint": "添加「技能集」后,勾选该场景下启用的技能。", "scenes.subagents.hint": "添加「子智能体绑定」后,勾选本场景可调用的人设。", "scenes.subagents.empty": "还没有人设——到「子智能体」页创建。",
550
- "scenes.filter.skills": "筛选技能(名称)", "scenes.filter.subagents": "筛选人设(名称或描述)", "scenes.filter.servers": "筛选服务器",
551
- "scenes.mem.hint": "添加「记忆」段后,勾选本场景要注入的记忆(只列本场景自己的记忆;全局记忆恒定注入、不需要在这里配置);不勾的记忆不会进系统提示词(文件保留)。",
707
+ "scenes.filter.skills": "筛选技能/目录名称", "scenes.filter.subagents": "筛选人设(名称或描述)", "scenes.filter.servers": "筛选服务器",
708
+
552
709
  "scenes.mem.title": "记忆", "scenes.mem.search": "在本场景内筛选记忆(名称或描述)",
553
710
  "scenes.mem.noMatch": "没有匹配项", "scenes.mem.emptyScene": "该场景还没有记忆",
554
711
  "memory.mode.current": "当前模式", "memory.mode.exit": "退出模式",
555
712
  "memory.archive.edit": "档案", "memory.archive.title": "场景档案",
556
713
  "memory.archive.tools": "MCP 工具集", "memory.archive.skills": "技能集", "memory.archive.subagents": "子智能体绑定", "memory.archive.memories": "记忆",
557
714
  "memory.archive.removeSection": "移除段", "memory.archive.save": "保存到场景",
558
- "memory.archive.addTools": "+ 添加 MCP 工具集", "memory.archive.addSkills": "+ 添加技能集", "memory.archive.addSubagents": "+ 添加子智能体绑定", "memory.archive.addMemories": "+ 添加记忆",
715
+ "memory.archive.addTools": "+ 添加 MCP 工具集", "memory.archive.addSkills": "+ 添加技能集", "memory.archive.addSubagents": "+ 添加子智能体绑定",
559
716
  "memory.archive.stale": "失效项(已不存在,已跳过): {items}",
560
- "memory.result.archiveSaved": "已保存场景档案:{name}", "memory.result.modeSet": "已进入模式:{name}", "memory.result.modeExited": "已退出模式",
561
- "memory.desc": "管理场景记忆:启用场景里的记忆自动进入系统提示词;场景留空 = 全局注入。",
562
- "memory.btn.refresh": "刷新", "memory.btn.new": "新建记忆", "memory.btn.create": "创建", "memory.btn.save": "保存",
717
+ "memory.result.archiveSaved": "已保存场景档案:{name}", "memory.result.modeSet": "已进入模式:{name}", "memory.result.modeSwitched": "已切换 {mcp} 台服务器 / {skills} 个来源", "memory.result.modeExited": "已退出模式",
718
+ "memory.desc": "管理记忆:启用的记忆进入系统提示词,可按场景分别启用。",
719
+ "memory.btn.refresh": "刷新", "memory.btn.new": "新增记忆", "memory.btn.create": "创建", "memory.btn.save": "保存",
563
720
  "memory.btn.diagnose": "体检", "memory.btn.delete.confirm": "移入回收站",
564
721
  "memory.diagnose.title": "记忆体检", "memory.diagnose.summary": "{rules} 条记忆 · {scenes} 个场景 · {issues} 个问题", "memory.diagnose.clean": "未发现问题,一切正常。",
565
722
  "memory.diag.error": "错误", "memory.diag.warning": "警告", "memory.diag.info": "提示",
@@ -572,10 +729,10 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
572
729
  "memory.scene.global": "全局", "memory.scene.count": "{count} 条记忆", "memory.scene.new": "新建记忆", "memory.scene.browse": "选择已有",
573
730
  "memory.scene.empty": "该场景暂无记忆",
574
731
  "memory.scene.orphan": "未归属场景", "memory.scene.orphan.tag": "不会注入", "memory.scene.orphan.hint": "这些记忆直接放在 memories/ 根层,没有归属场景,因此不会进入系统提示词。请把它们移入某个场景目录(或放到 memories/global/ 作为「全局」记忆)。",
575
- "memory.scene.edit": "改描述", "memory.scene.editTitle": "修改场景", "memory.scene.editHelp": "只改场景的名称显示与描述,记忆文件不受影响。",
732
+ "memory.scene.edit": "改描述", "memory.scene.editTitle": "修改场景", "memory.scene.editHelp": "名称就是它的记忆目录名;改名会连目录、档案与绑定一起改,记忆正文不动。",
576
733
  "memory.scene.field.name": "场景名", "memory.scene.field.name.placeholder": "例如 办公",
577
- "memory.scene.field.name.hint": "创建后不可改名。",
578
- "memory.scene.field.name.lock": "场景名创建后不可修改",
734
+ "memory.scene.field.name.hint": "名称就是它的一级目录名,之后仍可修改。",
735
+ "memory.scene.field.name.lock": "改名会一起改掉记忆目录、场景档案与启用集合;记忆正文不动。",
579
736
  "memory.scene.field.desc": "描述(可选)", "memory.scene.field.desc.placeholder": "一句话说明这个场景是干什么的",
580
737
  "memory.search.placeholder": "搜索名称 / 描述 / 场景", "memory.filter.all": "全部场景",
581
738
  "memory.empty": "还没有记忆,点「新建记忆」", "memory.empty.search": "没有匹配的记忆",
@@ -599,54 +756,68 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
599
756
  "memory.trash.count": "{count} 条已删除的记忆", "memory.trash.deletedAt": "删除于 {time}",
600
757
  "memory.trash.restore": "恢复", "memory.trash.purge": "永久删除",
601
758
  "memory.trash.confirmTitle": "永久删除?", "memory.trash.confirmDesc": "「{name}」将永久删除,无法恢复",
602
- "memory.btn.newScene": "新建场景", "memory.btn.deleteScene": "删除场景", "memory.btn.saveScene": "保存修改",
759
+ "memory.btn.newScene": "新增场景", "memory.btn.deleteScene": "删除场景", "memory.btn.saveScene": "保存修改",
603
760
  "memory.scene.createTitle": "新建场景",
604
- "memory.deleteScene.title": "删除场景?", "memory.deleteScene.desc": "将删除「{name}」的场景记录与空目录(里面还有记忆或正处在当前模式时不能删;「全局」是保留场景,不可删除)",
605
- "memory.result.sceneCreated": "已创建场景:{name}", "memory.result.sceneUpdated": "已保存场景:{name}", "memory.result.sceneRemoved": "已删除场景:{name}",
761
+ "memory.deleteScene.title": "删除场景?", "memory.deleteScene.desc": "把「{name}」连同它下面全部记忆(含未启用的)一起移入回收站,可在回收站整条恢复;正在使用的场景不能删。",
762
+ "memory.result.sceneCreated": "已创建场景:{name}", "memory.result.sceneUpdated": "已保存场景:{name}", "memory.result.sceneRemoved": "已删除场景:{name}", "memory.result.sceneRemoved.withMemories": "已删除场景:{name}(连同 {count} 项记忆,可在回收站整条恢复)",
606
763
  "memory.table.name": "记忆名称与描述", "memory.table.tags": "标记", "memory.table.status": "状态",
607
- "error.rules.invalidArgs": "缺少参数:需要 all:true(回到全部启用)或 scenes:[...](显式收窄)", "error.rules.invalidGroup": "场景/分组名不合法(非空、≤64 字符、不含路径分隔符与 < > : \" | ? *、不以 . 开头)", "error.rules.invalidName": "记忆名不合法(非空、≤64 字符、不含路径分隔符与 < > : \" | ? *、不以 . 开头)", "error.rules.descriptionRequired": "描述不能为空", "error.rules.descriptionTooLong": "描述过长(不能超过 500 字符)", "error.rules.bodyRequired": "正文不能为空", "error.rules.tooLarge": "规则内容过大", "error.rules.shadowed": "规则被同名 bundle 遮蔽,无法写入", "error.rules.notFound": "规则不存在", "error.rules.budgetExceeded": "场景记忆段超出预算", "error.rules.ioFailed": "文件操作失败", "error.rules.sceneNotEmpty": "场景不为空,无法删除", "error.rules.notBundle": "该记忆是 flat(单文件),不能带附件", "error.rules.noFiles": "没有选择附件", "error.rules.emptyFile": "附件内容为空", "error.rules.fileTooLarge": "附件过大(单个上限 {limit} MB)", "error.rules.tooManyFiles": "一次最多 {limit} 个附件", "error.rules.sceneInMode": "该场景正处在当前模式,请先退出模式再删除",
764
+ "error.rules.invalidArgs": "缺少参数:需要 all:true(回到全部启用)或 scenes:[...](显式收窄)", "error.rules.invalidGroup": "场景/分组名不合法(非空、≤64 字符、不含路径分隔符与 < > : \" | ? *、不以 . 开头)", "error.rules.invalidName": "记忆名不合法(非空、≤64 字符、不含路径分隔符与 < > : \" | ? *、不以 . 开头)", "error.rules.descriptionRequired": "描述不能为空", "error.rules.descriptionTooLong": "描述过长(不能超过 500 字符)", "error.rules.bodyRequired": "正文不能为空", "error.rules.tooLarge": "规则内容过大", "error.rules.shadowed": "规则被同名 bundle 遮蔽,无法写入", "error.rules.notFound": "规则不存在", "error.rules.budgetExceeded": "场景记忆段超出预算", "error.rules.ioFailed": "文件操作失败", "error.rules.nameTaken": "目标名称已被占用", "error.rules.sceneNotEmpty": "场景不为空,无法删除", "error.rules.notBundle": "该记忆是 flat(单文件),不能带附件", "error.rules.noFiles": "没有选择附件", "error.rules.emptyFile": "附件内容为空", "error.rules.fileTooLarge": "附件过大(单个上限 {limit} MB)", "error.rules.tooManyFiles": "一次最多 {limit} 个附件", "error.rules.sceneInMode": "该场景正处在当前模式,请先退出模式再删除",
608
765
  "memory.archive.emptySection": "该段已定义但没有勾选任何条目 = 全部停用", "memory.archive.emptySubagents": "该段已定义但没有勾选任何条目 = 不限制(全部人设可用)",
609
- "subagents.title": "子智能体", "subagents.desc": "管理人设:一个人设一个文件,正文即子代理的系统提示词。",
610
- "prompts.desc": "管理全局指令基线:应用后写入 AGENTS.md,新会话生效。",
766
+ "subagents.title": "子智能体", "subagents.stat.total": "个子智能体", "subagents.stat.limited": "个有工具限制", "subagents.desc": "管理子智能体:新建、导入人设,正文即子代理的系统提示词。",
767
+ "prompts.desc": "管理提示词:预设全局指令基线,应用后写入 AGENTS.md,下一轮对话生效。",
611
768
  "sessions.desc": "管理已归档会话:可恢复或永久删除,超保留期自动清理。",
612
- "subagents.new": "新建人设", "subagents.empty": "还没有人设——点「新建人设」创建第一个。",
769
+ "subagents.new": "新增子智能体", "subagents.empty": "还没有子智能体——点「新增子智能体」创建第一个。",
613
770
  "subagents.create": "新建人设", "subagents.edit": "编辑人设",
614
771
  "subagents.field.name": "人设名",
615
772
  "subagents.field.description": "描述", "subagents.field.description.placeholder": "例如 擅长 Java 后端实现与重构",
616
773
  "subagents.field.model": "模型", "subagents.field.model.placeholder": "自定义模型 id", "subagents.field.model.hint": "留空继承主会话", "subagents.field.provider": "模型来源", "subagents.field.provider.placeholder": "sensenova", "subagents.field.provider.hint": "「模型来源」与「模型」是一对:只填模型会落在主会话的来源上,跨来源会解析失败。",
617
774
  "subagents.model.inherit": "继承主会话(不指定)", "subagents.model.customOption": "自定义 / 目录里没有…",
618
- "subagents.field.tools": "工具白名单", "subagents.field.tools.hint": "勾选 = 子代理只保留这些工具;一个都不勾 = 不限制(基础组合全集)。",
619
- "subagents.field.toolsDeny": "工具黑名单", "subagents.field.toolsDeny.hint": "勾选 = 从子代理可见的工具里移除这些(优先级高于白名单)。适合「除了 bash 什么都能用」这类配置。",
620
- "subagents.tools.chosen": "未选择 = 不限制", "subagents.tools.filter": "筛选工具名", "subagents.tools.empty": "宿主没有回传任何可选工具(模型可能不支持工具目录枚举)",
621
- "subagents.tools.unavailable": "当前会话不可见", "subagents.tools.current": "当前可用", "subagents.tools.groupCurrent": "当前会话可用", "subagents.tools.groupOther": "其它 Agent 预设里可用",
622
- "subagents.tools.count": "已选 {count} 个", "subagents.tools.remove": "移除",
623
- "subagents.adv.title": "高级选项", "subagents.adv.inherit": "继承主会话", "subagents.adv.summary": "模型 {model} · 白名单 {allow} · 黑名单 {deny}", "subagents.adv.note": "工具按官方 Agent 预设分组展示;同名工具可能属于多个预设,勾选后仅在对应预设下生效。", "subagents.adv.loadFailed": "读取候选数据失败(模型目录 / 工具清单);仍可手动填写。",
775
+ "subagents.field.modes": "工具限制(按 Agent 预设)",
776
+ "subagents.field.modes.hint": "只有当前模式那一行生效,其他模式用该模式的全部工具。",
777
+ "subagents.mode.startAllow": "启动白名单", "subagents.mode.startDeny": "启动黑名单", "subagents.mode.stopAllow": "关闭白名单", "subagents.mode.stopDeny": "关闭黑名单",
778
+ "subagents.mode.off": "未限制", "subagents.mode.empty": "已启动但没勾选(= 不限制)",
779
+ "subagents.mode.allowOn": "白名单 {count} 个", "subagents.mode.denyOn": "黑名单 {count} 个",
780
+ "subagents.mode.allowEmpty": "一个都没勾 = 不限制(该模式的全部工具都能用)",
781
+ "subagents.mode.denyEmpty": "一个都没勾 = 不限制(该模式的全部工具都能用)",
782
+ "subagents.mode.noTools": "读不到这个模式的工具清单",
783
+ "subagents.mode.broken": "这个预设已损坏,读不到工具清单",
784
+ "subagents.mode.noPresets": "宿主没有回传 Agent 预设名单",
785
+ "subagents.legacy.note": "这个文件里还有一份旧写法的工具限制(白名单 {allow} 个 / 黑名单 {deny} 个),它对所有模式都生效。",
786
+ "subagents.legacy.convert": "转换到按模式设置",
787
+ "subagents.legacy.hint": "转换会把旧名单搬进你选的模式;不点它,文件里的旧键原样保留(运行时照旧生效)。",
788
+ "subagents.tools.filter": "筛选工具名", "subagents.tools.remove": "移除",
789
+ "subagents.adv.title": "高级选项", "subagents.adv.inherit": "继承主会话", "subagents.adv.summary": "模型 {model} · 模式限制 {modes} 项 · 旧格式 白名单 {allow} / 黑名单 {deny}", "subagents.adv.note": "白名单 = 子代理只能用勾选的工具;黑名单 = 除勾选的以外都能用。MCP 工具不在候选里:子代理始终能用当前在跑的 MCP。", "subagents.adv.loadFailed": "读取候选数据失败(模型目录 / 工具清单);仍可手动填写。",
790
+ "preset.name.standard": "标准模式", "preset.name.ptc": "PTC 模式", "preset.name.minimal": "极简模式", "preset.name.cordis": "创造模式",
791
+ "preset.short.standard": "标准", "preset.short.ptc": "PTC", "preset.short.minimal": "极简", "preset.short.cordis": "创造",
624
792
  "subagents.field.body": "人设提示词", "subagents.field.body.placeholder": "写下这个人设的身份、职责与工作方式…",
625
793
  "subagents.result.saved": "已保存人设:{name}", "subagents.result.deleted": "已删除人设:{name}",
626
- "subagents.delete.title": "删除人设?", "subagents.delete.desc": "将删除「{name}」的人设文件,不可恢复",
627
- "subagents.import": "导入", "subagents.import.title": "导入人设", "subagents.import.hint": "支持 .md 或 .zip(可多选、可拖入):一个 .md = 一个人设,文件名即人设名;同名自动跳过。",
794
+ "subagents.delete.title": "删除人设?", "subagents.delete.desc": "将把「{name}」的人设文件移入回收站(子智能体页的「回收站」里可以恢复)。",
795
+ "subagents.import": "导入子智能体", "subagents.import.title": "导入子智能体", "subagents.toggle": "启用子智能体", "subagents.disabled": "已停用", "subagents.disabled.hint": "停用后不注入系统提示词,subagent_list / subagent_run 也看不到;文件保留,随时可再打开。",
628
796
  "subagents.result.imported": "已导入 {count} 个人设:{names}",
629
797
  "memory.import": "导入记忆", "memory.import.title": "导入记忆", "memory.import.scene": "导入到场景",
630
- "memory.import.sceneHint": "留空 = 全局;zip 内的目录名会当作场景名。",
631
- "memory.import.hint": "支持 .md 或 .zip(可多选、可拖入):一个 .md = 一条记忆;zip 内 <场景>/<名>/SKILL.md 按 bundle 导入,同层文件作附件;同名自动跳过。",
632
798
  "memory.result.imported": "已导入 {count} 条记忆:{names}",
633
799
  "memory.import.scenesCreated": "为此新建了场景:{names}",
634
800
  "memory.scene.global.hint": "全局",
635
- "import.pick": "选择文件", "import.selected": "已选 {count} 个文件", "import.clear": "清空选择", "import.submit": "安装",
801
+ "import.clear": "清空选择", "import.submit": "导入",
636
802
  "import.none": "没有导入任何文件(全部被跳过)", "import.skipped": "已跳过:{items}",
637
803
  "error.import.noFiles": "没有选择要导入的文件",
638
804
  "nav.title": "工具",
639
- "agm.current.external": "(已外部修改)", "agm.current.none": "(未设置)", "agm.current.label": "当前:{label}", "agm.loading": "加载中…", "agm.empty": "暂无预设,点「新建预设」创建", "agm.btn.delete.blocked": "正在生效的提示词预设不能删除;先启用别的场景、换绑提示词,或先把别的预设应用上去。", "agm.active": "生效中", "agm.active.hint.scene": "当前生效:场景「{scene}」绑定的提示词预设", "agm.active.hint.file": "当前生效:AGENTS.md 就是这一份", "agm.file.applied": "文件里是它", "agm.file.applied.hint": "AGENTS.md 当前内容是它;启用的场景绑了别的提示词,切换场景时已按场景把文件改成那一份", "agm.current.byScene": "{id}(场景「{scene}」)", "agm.apply.hint": "应用后新会话生效,当前会话不变", "agm.btn.reapply": "重新应用", "agm.btn.apply": "应用", "agm.applyModal.title": "应用预设", "agm.edit.title": "编辑预设", "agm.remove.title": "删除预设", "agm.btn.edit": "编辑", "agm.btn.delete": "删除", "agm.btn.new": "新建预设", "agm.btn.import": "导入", "agm.btn.save": "保存", "agm.btn.create": "创建", "agm.btn.confirmRemove": "确认删除", "agm.btn.confirmApply": "确认应用", "agm.field.id": "id(目录名)", "agm.field.id.hint": "中文、空格、下划线、点都可以;不能含 / \\ < > : \" | ? *,不能以点开头、不能以点或空格结尾,≤64 字符。改 id = 目录改名,场景绑定会一起改。", "agm.field.id.placeholder": "例如:工作基线", "agm.field.content": "内容", "agm.field.content.placeholder": "直接写这份预设的正文(Markdown);留空 = 用空白模板", "agm.field.copyFrom": "或从现有预设复制", "agm.field.copyFrom.none": "不复制(空白模板)", "agm.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": "该目录的工作区登记已被删除;分组按会话目录重建,重新登记后会自动并回同一组", "hist.group.unregistered": "未登记的工作区", "hist.group.unregistered.hint": "该目录当前在 DSH 里没有工作区登记;分组按会话目录重建", "hist.btn.registerWs": "重新登记为工作区", "hist.btn.registerWs.hint": "重新登记这个目录为工作区,并把该目录下已有的会话挂回该组(只写工作区记账,不移动、不删除文件与会话)", "hist.register.title": "重新登记为工作区?", "hist.register.body": "将为「{title}」在 DSH 中重新创建一条工作区登记,并把该目录下已有的会话挂回这个工作区(恢复其分组)。只写工作区记账,不移动、不删除任何文件与会话。", "hist.register.confirm": "确认登记", "hist.register.done.title": "已重新登记", "hist.register.done.body": "工作区「{title}」已登记;该目录下的归档会话会在刷新后并入这一组。", "hist.register.done.attached": "已把该目录下 {count} 个已有会话挂回这个工作区。", "hist.register.done.skipped": "{count} 个会话未能挂回:{reason}", "hist.err.register": "登记失败", "scenes.field.prompt": "提示词预设", "scenes.agents.note.applied": "已把「{id}」写入 AGENTS.md", "scenes.agents.note.restored": "已恢复进场景前的 AGENTS.md", "scenes.agents.note.error": "AGENTS.md 未写入:{reason}", "scenes.field.prompt.hint": "启用这个场景时,就用这份预设作为当前的全局指令基线。默认选中当前生效的那份。", "scenes.prompt.activeTag": "生效中", "scenes.prompt.noPresets": "还没有提示词预设;可先去「提示词」页新建一个。", "scenes.prompt.bound": "提示词:{id}", "scenes.prompt.live": "提示词生效中:{id}", "scenes.prompt.missing": "预设不存在:{id}", "scenes.prompt.tag.hint": "这个场景绑定的提示词预设", "scenes.enable.blocked": "已启用场景「{name}」;先关掉它才能启用别的", "scenes.result.created.collapsed": "场景「{name}」已创建,默认不启动(原先的「全部启用」已收敛为单选)", "scenes.legacyAll": "检测到 {count} 个场景同时处于启用状态(历史「全部启用」遗留);除「全局」外同时只能启用一个。", "scenes.legacyAll.fix": "收敛为单选", "agm.scene.tag": "场景「{scene}」正在使用", "agm.scene.tag.hint": "由这个场景绑定,随场景开关生效", "agm.scene.missing": "场景「{scene}」绑定了它,但文件不存在", "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.selected": "个已选", "hist.search.placeholder": "搜索标题 / 会话 ID / 项目路径", "hist.loading": "加载中…", "hist.empty.search": "无匹配的归档会话", "hist.empty.none": "暂无归档会话", "hist.import.hint": "导入其他 Agent 的对话记录,生成可继续对话的新会话", "hist.import.cwd": "项目目录(可留空)", "hist.import.cwd.placeholder": "绝对路径,可留空(会话归入未分组)", "hist.import.drop": "选择或拖入对话文件", "hist.import.done": "已创建会话 {id}({count} 条消息),可在 DSH 会话列表中继续对话。", "hist.export.hint": "导出选中会话为转录文件,可再次导入", "hist.export.dir": "导出目录(绝对路径,自动创建)", "hist.export.browse": "浏览并选择文件夹", "hist.btn.browse": "选择", "hist.export.pickDir": "选择导出目录", "hist.export.format": "格式", "hist.export.scope": "会话范围", "hist.export.workspace": "工作区", "hist.export.sessions": "会话({count} 个)", "hist.export.loading": "加载会话列表…", "hist.cwd.missing": "⚠ 工作区目录已不存在:", "hist.tag.archived": "已归档", "hist.tag.live": "未归档", "hist.export.empty": "没有符合条件的会话", "hist.result.exported": "已导出 {count} 个会话到 {dir}", "hist.result.skipped": ";跳过 {count} 个({ids})", "hist.result.archived": "已归档 {count} 个会话到 History,可到 History 页继续管理", "hist.btn.archiveSelected": "归档所选", "hist.btn.archiveSelected.title": "把选中的会话收进 History,纳入保留期管理", "hist.export.busy": "处理中…", "hist.btn.exportSelected": "导出 {count} 个会话", "hist.error.title": "操作失败", "hist.error.unknown": "未知错误",
805
+ "agm.stat.total": "个提示词", "agm.stat.active": "个生效中", "agm.loading": "加载中…", "agm.empty": "暂无提示词,点「新增提示词」创建", "agm.btn.delete.blocked": "正在生效的提示词预设不能删除;先启用别的场景、换绑提示词,或先把别的预设应用上去。", "agm.active": "生效中", "agm.active.hint.scene": "当前生效:场景「{scene}」绑定的提示词预设", "agm.active.hint.file": "当前生效:AGENTS.md 就是这一份", "agm.file.applied": "文件里是它", "agm.file.applied.hint": "AGENTS.md 当前内容是它;启用的场景绑了别的提示词,切换场景时已按场景把文件改成那一份", "agm.apply.hint": "应用后写入 AGENTS.md;宿主每轮重读该文件,下一轮对话生效", "agm.btn.reapply": "重新应用", "agm.btn.apply": "应用", "agm.applyModal.title": "应用预设", "agm.edit.title": "编辑预设", "agm.remove.title": "删除预设", "agm.btn.edit": "编辑", "agm.btn.delete": "删除", "agm.btn.new": "新增提示词", "agm.btn.import": "导入提示词", "agm.result.imported": "已导入 {count} 份预设:{names}", "agm.btn.save": "保存", "agm.btn.create": "创建", "agm.btn.confirmRemove": "确认删除", "agm.btn.confirmApply": "确认应用", "agm.field.id": "id(目录名)", "agm.field.id.hint": "中文、空格、点都可以;不能含斜杠、反斜杠与 : * ? < > 等符号;≤64 字符。改 id 等于目录改名。", "agm.field.id.placeholder": "例如:工作基线", "agm.field.content": "内容", "agm.field.content.placeholder": "直接写这份预设的正文(Markdown);留空 = 用空白模板", "agm.field.copyFrom": "或从现有预设复制", "agm.field.copyFrom.none": "不复制(空白模板)", "agm.field.desc": "描述(只给使用者看)", "agm.field.desc.placeholder": "这份预设是干什么的、什么时候用它", "agm.result.renamed": "已保存,id 改为「{id}」", "agm.result.rebound": "(同步了 {count} 个场景的绑定)", "agm.remove.suffix": " ?此操作不可恢复。", "agm.apply.prefix": "将把 ", "agm.apply.suffix": " 的内容写入 ~/.dsh/AGENTS.md。", "agm.apply.note": "下一轮对话生效;当前内容已备份到 __last-applied__(保留最近 5 代)", "hist.err.load": "加载失败", "hist.err.unarchive": "恢复失败", "hist.err.delete": "删除失败", "hist.err.retention": "设置失败", "hist.err.import": "导入失败", "hist.err.sessions": "加载会话列表失败", "hist.err.export": "导出失败", "hist.err.archive": "归档失败", "hist.retention.forever": "永久保留", "hist.retention.days": "{count} 天", "hist.arch.all": "全部会话", "hist.arch.archived": "仅已归档", "hist.arch.live": "仅未归档", "hist.arch.missing": "仅目录丢失", "hist.ws.all": "全部工作区", "hist.ungrouped": "未分组", "hist.group.removed": "已移除", "hist.group.removed.hint": "该目录当前在 DSH 里没有工作区登记;分组按会话目录重建,重新登记后会自动并回同一组", "hist.btn.registerWs": "重新登记", "hist.btn.registerWs.hint": "重新登记这个目录,并把该目录下已有的会话挂回该组(只写工作区记账,不移动、不删除文件与会话)", "hist.register.title": "重新登记?", "hist.register.body": "将为「{title}」在 DSH 中重新创建一条工作区登记,并把该目录下已有的会话挂回这个工作区(恢复其分组)。只写工作区记账,不移动、不删除任何文件与会话。", "hist.register.confirm": "确认登记", "hist.register.done.title": "已重新登记", "hist.register.done.body": "工作区「{title}」已登记;该目录下的归档会话会在刷新后并入这一组。", "hist.register.done.attached": "已把该目录下 {count} 个已有会话挂回这个工作区。", "hist.register.done.skipped": "{count} 个会话未能挂回:{reason}", "hist.err.register": "登记失败", "scenes.field.prompt": "提示词预设", "scenes.agents.note.applied": "已把「{id}」写入 AGENTS.md", "scenes.agents.note.restored": "已恢复进场景前的 AGENTS.md", "scenes.agents.note.error": "AGENTS.md 未写入:{reason}", "scenes.field.prompt.hint": "启用这个场景时,就用这份预设作为当前的全局指令基线。默认选中当前生效的那份。", "scenes.prompt.activeTag": "生效中", "scenes.prompt.noPresets": "还没有提示词预设;可先去「提示词」页新建一个。", "scenes.prompt.bound": "提示词:{id}", "scenes.prompt.live": "提示词生效中:{id}", "scenes.prompt.missing": "预设不存在:{id}", "scenes.prompt.tag.hint": "这个场景绑定的提示词预设", "scenes.enable.blocked": "已启用场景「{name}」;先关掉它才能启用别的", "scenes.result.created.collapsed": "场景「{name}」已创建,默认不启动(原先的「全部启用」已收敛为单选)", "scenes.legacyAll": "检测到 {count} 个场景同时处于启用状态(历史「全部启用」遗留);除「全局」外同时只能启用一个。", "scenes.legacyAll.fix": "收敛为单选", "agm.scene.tag": "场景「{scene}」正在使用", "agm.scene.tag.hint": "由这个场景绑定,随场景开关生效", "agm.scene.missing": "场景「{scene}」绑定了它,但文件不存在", "hist.restore.note.registered": "已恢复 {count} 个会话;工作区「{title}」已重新登记,会话已挂回该组。", "hist.restore.note.attached": "已恢复 {count} 个会话,并挂回工作区「{title}」。", "hist.restore.note.skip": "{count} 个会话的工作区归属未能恢复:{reason}", "hist.purge.title": "永久删除?", "hist.purge.one": "永久删除会话「{title}」及其全部记录,不可恢复", "hist.purge.batch": "永久删除所选 {count} 个会话及其全部记录,不可恢复", "hist.btn.restore": "恢复", "hist.btn.purge": "永久删除", "hist.btn.confirmPurge": "确认删除", "hist.selectGroup": "全选组 {title}", "hist.group.count": "{count} 个", "hist.selectAll": "全选", "hist.deselectAll": "取消全选", "hist.btn.import": "导入会话", "hist.btn.export": "导出会话", "hist.selected": "已选 {count} 项", "hist.btn.restoreSelected": "恢复所选 ({count})", "hist.btn.deleteSelected": "删除所选 ({count})", "hist.stat.archived": "个归档", "hist.stat.projects": "个项目", "hist.stat.retention": "天保留", "hist.search.placeholder": "搜索标题 / 会话 ID / 项目路径", "hist.loading": "加载中…", "hist.empty.search": "无匹配的归档会话", "hist.empty.none": "暂无归档会话", "hist.import.cwd": "项目目录(可留空)", "hist.import.cwd.placeholder": "绝对路径,可留空(会话归入未分组)", "hist.import.drop": "选择或拖入对话文件", "hist.import.done": "已创建会话 {id}({count} 条消息),可在 DSH 会话列表中继续对话。", "hist.export.hint": "导出选中会话为转录文件,可再次导入", "hist.export.dir": "导出目录(绝对路径,自动创建)", "hist.export.browse": "浏览并选择文件夹", "hist.btn.browse": "选择", "hist.export.pickDir": "选择导出目录", "hist.export.format": "格式", "hist.export.scope": "会话范围", "hist.export.workspace": "工作区", "hist.export.sessions": "会话({count} 个)", "hist.export.loading": "加载会话列表…", "hist.cwd.missing": "⚠ 工作区目录已不存在:", "hist.tag.archived": "已归档", "hist.tag.live": "未归档", "hist.export.empty": "没有符合条件的会话", "hist.result.exported": "已导出 {count} 个会话到 {dir}", "hist.result.skipped": ";跳过 {count} 个({ids})", "hist.result.archived": "已归档 {count} 个会话到 History,可到 History 页继续管理", "hist.btn.archiveSelected": "归档所选", "hist.btn.archiveSelected.title": "把选中的会话收进 History,纳入保留期管理", "hist.export.busy": "处理中…", "hist.btn.exportSelected": "导出 {count} 个会话", "hist.error.title": "操作失败", "hist.error.unknown": "未知错误",
640
806
  },
641
807
  en: {
642
- "title": "Skills", "desc": "Manage skills on this machine: enable, import, create and trash; pick which copy of a shared name is active.", "link.project": "GitHub", "link.feedback": "Issues",
643
- "btn.create": "Create skill", "btn.import": "Import", "btn.refresh": "Refresh", "btn.cancel": "Cancel", "btn.close": "Close", "btn.detail": "View details", "btn.trash": "Move to trash", "btn.restore": "Restore", "btn.delete.forever": "Delete forever", "btn.file.pick": "Choose file", "btn.folder.pick": "Choose folder", "btn.import.now": "Install", "btn.create.now": "Create skill", "btn.disable": "Disable", "btn.enable": "Enable", "btn.open.editor": "Open in editor", "btn.activate": "Use this one", "btn.activate.title": "Make this source's copy the active one for this shared name and enable it (source files are never touched)", "btn.unprefer": "Auto again", "btn.unprefer.title": "Clear the manual choice and go back to automatic source priority",
808
+ "title": "Skills", "desc": "Manage skills: enable, import, create, trash.", "link.project": "GitHub", "link.feedback": "Issues",
809
+ "btn.create": "New skill", "btn.import": "Import skills", "btn.refresh": "Refresh", "btn.refreshing": "Refreshing…", "btn.cancel": "Cancel", "btn.close": "Close", "export.hint": "Exports a zip into the directory you choose; source files are read-only and nothing is modified.", "export.outDir": "Output directory", "export.outDir.placeholder": "e.g. D:////backup", "export.pickDir": "Choose output directory", "export.submit": "Export", "export.busy": "Exporting…", "export.done": "Exported {count} file(s) to {path}", "scenes.lock.lock": "Lock", "scenes.lock.unlock": "Unlock", "scenes.lock.tag": "Locked", "scenes.lock.hint": "Locking makes all five domains (MCP / skills / subagents / memories / prompts) read-only; starting or stopping scenes still works", "scenes.lock.blockedEdit": "Scene is locked — unlock it to make changes", "scenes.lock.notActive": "Scene is not active — start it before locking", "scenes.lock.blockedExit": "Scene is locked — unlock it before turning it off", "lock.banner": "A scene is locked: MCP, skills, subagents, memories and prompts are frozen. Unlock it on the Scenes page to make changes.", "export.empty": "Nothing to export", "export.pickAll": "Select all", "export.pickNone": "Clear", "export.group.count": "{count}", "export.missing": "; skipped {count} (missing on disk): {names}", "export.skills": "Export skills", "export.subagents": "Export subagents", "export.presets": "Export prompts", "export.memories": "Export memories", "btn.detail": "View details", "btn.trash": "Move to trash", "btn.restore": "Restore", "btn.delete.forever": "Delete forever", "btn.file.pick": "Choose file", "btn.folder.pick": "Choose folder", "btn.create.now": "Create skill", "btn.disable": "Disable", "btn.enable": "Enable", "btn.open.editor": "Open in editor", "btn.activate": "Use this one", "btn.activate.title": "Make this source's copy the active one for this shared name and enable it (source files are never touched)", "btn.unprefer": "Auto again", "btn.unprefer.title": "Clear the manual choice and go back to automatic source priority",
644
810
  "btn.custom.add": "Add folder",
645
811
  "btn.custom.remove": "Remove",
646
- "btn.source.remove": "Remove source", "btn.source.remove.title": "Stop reading this source (files stay on disk, reversible)", "btn.source.restore": "Read again",
647
- "removed.title": "Removed sources", "removed.count": "{count}", "removed.hint": "The plugin no longer reads these directories; every file and skill is still on disk — click “Read again” to bring one back",
812
+ "btn.source.remove": "Remove source", "btn.source.remove.title": "Stop reading this source (files stay on disk; restore it later from the trash)", "btn.source.restore": "Restore", "btn.source.forget": "Delete forever",
813
+ "trash.section.dirs": "Directories", "trash.section.skills": "Skills", "trash.section.dirs.sub": "No longer read — files on disk are untouched", "trash.section.dirs.empty": "No removed sources", "trash.section.skills.sub": "Files really were moved: restore brings them back, delete-forever removes them", "trash.section.presets.sub": "Preset folders were moved: restore brings them back, delete-forever removes them", "trash.section.scenes.sub": "Scene record, profile and its memories are all here: restore puts everything back", "trash.section.agents.sub": "Persona files were moved: restore brings them back, delete-forever removes them",
814
+ "trash.dirs.count.one": "{count} directory pending", "trash.dirs.count.other": "{count} directories pending",
815
+ "trash.row.summary": "{skills} skill(s) · {dirs} director(ies)",
816
+ "confirm.source.forget.title": "Delete this source record for good?", "confirm.source.forget.desc": "“{name}” is removed from the plugin entirely: it disappears from the source list and “Read again” cannot bring it back (you would have to add the folder again).",
817
+ "confirm.source.forget.hint": "Not a single byte is deleted on disk: {path}.",
818
+ "result.sourceForgotten": "Source record deleted for good: {name}",
648
819
  "confirm.source.remove.title": "Remove this source?", "confirm.source.remove.desc": "“{name}” will no longer be read: its skills disappear from this page and stop taking part in name resolution; your enable/disable choices are kept.",
649
- "confirm.source.remove.hint": "No file is deleted. Use “Read again” in this group to restore it.",
820
+ "confirm.source.remove.hint": "No file is deleted. Once removed it shows up under Trash → Directories, where “Read again” brings it back (or you can delete the record for good).",
650
821
  "result.sourceRemoved": "Source removed: {name} (no longer read)", "result.sourceRestored": "Reading again: {name}",
651
822
  "btn.openDir": "Browse",
652
823
  "custom.add.title": "Add custom skill folder",
@@ -664,14 +835,17 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
664
835
  "result.custom.removed": "Custom folder removed: {name}",
665
836
  "result.opened": "Opened with the system default app: {path}",
666
837
  "status.enabled": "Enabled", "status.disabled": "Disabled", "status.invalid": "Needs attention", "status.shadowed": "Shadowed", "status.shadowed.hint": "The copy “{name}” is the active one; click “Use this one” to switch to this source instead", "status.preferred": "Manually chosen", "status.readonly": "Source read-only", "status.notDeletable": "Cannot delete", "status.notDeletable.hint": "Deleting is available for plugin-managed sources (DSH skills / Imported skills / project .dsh/skills); external agent and custom directories are read-only", "status.manageable": "Manageable", "status.project": "Project scoped", "status.rank": "Rank {rank}", "status.source.on": "Enabled", "status.source.off": "Disabled", "status.bundle": "Bundle", "status.single": "Single file",
667
- "summary.total.one": "{count} skill", "summary.total.other": "{count} skills", "summary.enabled.one": "{count} enabled", "summary.enabled.other": "{count} enabled", "summary.disabled.one": "{count} disabled", "summary.disabled.other": "{count} disabled", "summary.issues.one": "{count} diagnostic", "summary.issues.other": "{count} diagnostics", "summary.group.one": "{count} skill", "summary.group.other": "{count} skills", "table.skill": "Skill name and description", "table.status": "Invocation status",
838
+ "summary.total.one": "{count} skill", "summary.total.other": "{count} skills", "summary.enabled.one": "{count} enabled", "summary.enabled.other": "{count} enabled", "summary.issues.one": "{count} diagnostic", "summary.issues.other": "{count} diagnostics", "summary.group.one": "{count} skill", "summary.group.other": "{count} skills", "table.skill": "Skill name and description", "table.status": "Invocation status",
668
839
  "filter.source": "Source", "filter.all": "All sources", "filter.option": "{name} ({count})", "search": "Search", "search.placeholder": "Search skill names or descriptions", "search.clear": "Clear search",
669
840
  "empty.search": "No matching skills", "empty.source": "This source is missing or has no skills", "loading": "Loading skills…", "note.missing": "No description provided", "source.toggle": "Toggle source", "skill.toggle": "Toggle skill",
670
841
  "source.external.note": "Read-only; toggles never rewrite source files", "source.dsh.note": "Create, import, trash; a default source, always read — it cannot be removed or toggled off",
671
842
  "detail.title": "Skill details", "detail.body": "Body", "detail.frontmatter": "Metadata", "detail.noFrontmatter": "This skill provides no metadata.", "detail.diagnostics": "Diagnostics", "detail.path": "Source file", "detail.noIssues": "No diagnostic issues found.",
672
843
  "create.title": "Create skill", "create.target": "Create in", "create.name": "Name", "create.name.placeholder": "e.g. code-review-helper", "create.description": "Description", "create.description.placeholder": "One sentence describing when to use it", "create.body": "Body (Markdown)", "create.body.placeholder": "Write the instructions, steps, and boundaries…", "create.chat.note": "create_skill in chat creates user-level skills",
673
- "import.title": "Import skill", "upload.drop.title": "Click or drop here", "upload.drop.copy": "Supported: .zip, a skill folder, or one SKILL.md", "upload.selected.one": "{count} file · {size}", "upload.selected.other": "{count} files · {size}", "upload.remove": "Remove selection", "upload.requirements": "File requirements", "upload.requirement.skill": "Archives and folders must contain SKILL.md", "upload.requirement.frontmatter": "SKILL.md must include a YAML name and description", "upload.requirement.copy": "Import copies all content and never modifies the source", "upload.importing": "Installing…", "status.selected": "Selected", "select.file.invalid": "Choose a .zip archive or one SKILL.md.", "select.folder.invalid": "No SKILL.md was found in the selected folder.", "error.browse.absolute": "Folder path must be absolute: {path}", "error.browse.unreadable": "Could not read folder: {path}", "error.browse.notDirectory": "Not a folder: {path}",
844
+ "import.title": "Import skill", "upload.drop.title": "Click or drop here", "upload.drop.copy": "Supported: .zip, a skill folder, or one SKILL.md", "upload.selected.one": "{count} file · {size}", "upload.selected.other": "{count} files · {size}", "upload.remove": "Remove selection", "upload.requirements": "File requirements", "upload.requirement.skill": "Archives and folders must contain SKILL.md", "upload.requirement.frontmatter": "SKILL.md must include a YAML name and description", "upload.requirement.copy": "Import copies all content and never modifies the source", "upload.requirement.persona.1": "Accepts .md / .zip (multi-select or drag in)", "upload.requirement.persona.2": "One .md = one persona; the file name is the persona name", "upload.requirement.persona.3": "Same names are skipped, never overwritten", "upload.requirement.memory.1": "Accepts .md / .zip (multi-select or drag in)", "upload.requirement.memory.2": "One .md = one memory; a folder with SKILL.md becomes a bundle memory (attachments included)", "upload.requirement.memory.3": "Folder names inside the zip are scenes; otherwise the scene field below applies (blank = global)", "upload.requirement.prompt.1": "Accepts .md (multi-select or drag in)", "upload.requirement.prompt.2": "One .md = one preset; the file name is the id", "upload.requirement.prompt.3": "A preset with the same name is skipped, never overwritten", "upload.requirement.session.1": "Accepts .jsonl / .json / .md / .txt (one file at a time)", "upload.requirement.session.2": "Transcripts from Claude Code / Cursor / Codex, or any plain text", "upload.requirement.session.3": "The project directory may be left blank (the session goes to Ungrouped)", "upload.importing": "Importing…", "status.selected": "Selected", "select.file.invalid": "Choose a .zip archive or one SKILL.md.", "select.folder.invalid": "No SKILL.md was found in the selected folder.", "error.browse.absolute": "Folder path must be absolute: {path}", "error.browse.unreadable": "Could not read folder: {path}", "error.browse.notDirectory": "Not a folder: {path}",
674
845
  "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}",
846
+ "trash.btn.open": "Trash", "trash.items.count": "{count} item(s)", "trash.purge.confirm": "Delete forever?",
847
+ "trash.group.agents": "Personas", "trash.group.scenes": "Scenes", "trash.group.presets": "Prompt presets",
848
+ "trash.restored": "Restored “{name}”", "trash.purged": "Permanently deleted “{name}”", "trash.loadFailed": "Could not read the trash",
675
849
  "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",
676
850
  "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}",
677
851
  "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 active workspace could not be read from the host, so its project skills are hidden: {path}",
@@ -685,13 +859,13 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
685
859
  "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})",
686
860
  "diagnostic.frontmatter.missing": "Missing complete YAML frontmatter", "diagnostic.name.missing": "Frontmatter is missing name", "diagnostic.name.invalid": "Skill name must be kebab-case: {name}", "diagnostic.description.missing": "Frontmatter is missing description", "diagnostic.invocation.invalid": "Invocation policy value is invalid", "diagnostic.shadowed": "Shadowed by higher-priority source {root}",
687
861
  "action.enable": "enable", "action.disable": "disable", "action.create": "create", "action.delete": "delete", "action.restore": "restore", "action.toggle": "enabling or disabling",
688
- "mcp.desc": "Manage this machine's MCP servers: add, enable/disable, restart, and per-tool switches.",
862
+ "mcp.desc": "Manage MCP: add, enable/disable, restart, and per-tool switches.",
689
863
  "mcp.level.all": "All levels", "mcp.level.project": "Profile level", "mcp.level.global": "Global", "mcp.level.loader": "Loaded",
690
864
  "mcp.stat.total": "server(s)", "mcp.stat.enabled": "enabled", "mcp.stat.tools": "tool(s)",
691
865
  "mcp.search": "Search servers", "mcp.search.placeholder": "Search a server name, URL or command",
692
- "mcp.btn.new": "Add server", "mcp.btn.add": "Add", "mcp.btn.save": "Save", "mcp.btn.detail": "Details", "mcp.btn.edit": "Edit", "mcp.btn.restart": "Restart", "mcp.btn.remove": "Delete",
866
+ "mcp.btn.new": "New MCP", "mcp.btn.add": "Add", "mcp.btn.save": "Save", "mcp.btn.detail": "Details", "mcp.btn.edit": "Edit", "mcp.btn.restart": "Restart", "mcp.btn.remove": "Delete",
693
867
  "mcp.btn.enableAll": "Enable all", "mcp.btn.disableAll": "Disable all",
694
- "mcp.loading": "Loading MCP servers…", "mcp.empty": "No MCP servers yet — click “Add server”.", "mcp.empty.search": "No matching server.",
868
+ "mcp.loading": "Loading MCP servers…", "mcp.empty": "No MCP servers yet — click “New MCP”.", "mcp.empty.search": "No matching server.",
695
869
  "mcp.servers.count": "{count} server(s)", "mcp.tools.count": "{count} tool(s)", "mcp.duplicate": "duplicate id",
696
870
  "mcp.table.name": "Server name and URL", "mcp.table.transport": "Transport and tools", "mcp.table.status": "Status",
697
871
  "mcp.live.notLoaded": "not loaded", "mcp.live.failed": "start failed", "mcp.live.stopped": "stopped", "mcp.live.loading": "loading", "mcp.live.noTools": "no tools", "mcp.live.running": "running",
@@ -708,8 +882,13 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
708
882
  "mcp.field.env": "Environment (one key=value per line)", "mcp.field.env.hint": "Use the platform's own path syntax (backslashes on Windows, / on macOS/Linux).",
709
883
  "mcp.field.headers": "Headers (one key=value per line)",
710
884
  "mcp.detail.title": "Server details: ", "mcp.detail.config": "Configuration", "mcp.detail.showSecret": "Reveal secrets", "mcp.detail.hideSecret": "Hide secrets",
885
+ "mcp.token.label": "Access token", "mcp.token.placeholder": "Same value as the host token", "mcp.token.save": "Save and retry",
886
+ "mcp.token.hint": "Plaintext secrets need the matching access token: set token in the host config (or the DSH_PLUGIN_TOOL_MANAGEMENT_TOKEN env var), restart DSH, then enter the same value here.",
887
+ "mcp.btn.compact": "Tidy patch", "mcp.btn.compact.title": "Clear the stale on/off records left in the patch file (each server's enabled/disabled state is unchanged)",
888
+ "mcp.compact.title": "Tidy the patch file", "mcp.compact.desc": "The config accumulates stale on/off records — leftovers from switching the same server on and off over time. This clears them so the file stays short and readable.",
889
+ "mcp.compact.confirm": "Tidy now", "mcp.compact.done": "Patch file tidied: cleared {count} stale on/off record(s); no server changed state", "mcp.compact.clean": "The patch file is already clean — nothing stale to clear",
711
890
  "mcp.detail.entryId": "Entry id", "mcp.detail.status": "Status", "mcp.detail.registered": "This server is registered in the loader.",
712
- "mcp.detail.note": "Note (local only)", "mcp.detail.note.placeholder": "e.g. fall back to B when A is unavailable", "mcp.detail.note.save": "Save note",
891
+ "mcp.detail.note": "Note (injected into the system prompt and sent to the model)", "mcp.detail.note.placeholder": "e.g. fall back to B when A is unavailable", "mcp.detail.note.save": "Save note",
713
892
  "mcp.detail.tools": "Tools ({count})",
714
893
  "mcp.field.headersShort": "Headers", "mcp.field.envShort": "Environment",
715
894
  "mcp.tools.loading": "Fetching the tool list…", "mcp.tools.loadFailed": "Could not load tools: ", "mcp.tools.none": "This server has no registered tools.",
@@ -719,11 +898,11 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
719
898
  "root.dsh": "DSH skills", "root.hub": "Imported skills", "root.agents": "Shared Agent", "root.ccswitch": "CC Switch", "root.projectDsh": "Project DSH", "root.projectAgents": "Project Agent", "root.codex": "Codex", "root.claude": "Claude", "root.gemini": "Gemini", "root.opencode": "OpenCode", "root.cursor": "Cursor",
720
899
  "memory.title": "Memories",
721
900
  "tabs.scenes": "Scenes", "tabs.skills": "Skills", "tabs.subagents": "Subagents", "tabs.prompts": "Prompts", "tabs.memory": "Memories", "tabs.sessions": "Sessions", "tabs.compat": "Host",
722
- "compat.title": "Host compatibility", "compat.desc": "How this plugin attaches to the running DSH: whether it loads the same module instances, which route each action takes, and which capabilities are degraded. Read-only; no data is touched.",
901
+ "compat.title": "Host compatibility", "compat.desc": "How this plugin attaches to DSH (read-only).",
723
902
  "compat.state.ok": "All good", "compat.state.degraded": "Degraded", "compat.state.blocked": "Needs attention",
724
903
  "compat.checking": "Checking…", "compat.recheck": "Check again", "compat.empty": "Not checked yet", "compat.failed": "Check failed",
725
904
  "compat.summary.ok": "host {version} · capabilities {ok}/{total} · all usable", "compat.summary.degraded": "host {version} · capabilities {ok}/{total} · {count} degraded",
726
- "compat.host": "Host version", "compat.peer": "Required range", "compat.verified": "Verified against", "compat.verified.note": "the release this plugin was tested with",
905
+ "compat.host": "Host version", "compat.peer": "Requires", "compat.verified": "Verified against", "compat.verified.note": "the release this plugin was tested with",
727
906
  "compat.caps": "Usable capabilities", "compat.caps.all": "all present", "compat.caps.degraded": "{count} unavailable",
728
907
  "compat.caps.substituted": "gaps covered by the plugin",
729
908
  "compat.ops": "Host action availability", "compat.ops.hint": "which route each action takes", "compat.ops.disabled": "{count} action(s) are unavailable right now (the affected buttons are disabled and explain why)",
@@ -737,35 +916,40 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
737
916
  "compat.fallback.native": "Falls back to native entry", "compat.fallback.blocked": "Affected buttons disabled",
738
917
  "compat.modules": "Module identity (plugin vs host)", "compat.modules.hint": "adapting only works on one shared module", "compat.module.same": "same module", "compat.module.separate": "separate copy (repair needed)", "compat.module.unknown": "not comparable",
739
918
  "compat.blockers": "Blockers (fix these)",
919
+ "compat.reach": "Preset injection reach", "compat.reach.hint": "the panel says injected — does the model actually see it?",
920
+ "compat.reach.mark.memory": "Memory", "compat.reach.mark.agentsMd": "Prompt", "compat.reach.mark.skill": "Skills", "compat.reach.mark.mcp": "MCP", "compat.reach.mark.subagent": "Subagents",
921
+ "compat.reach.default": "default",
922
+ "compat.reach.whyUnknown": "cannot tell", "compat.reach.broken": "preset broken",
923
+ "compat.reach.tools": "This plugin's model tools (including subagent_list / subagent_run) and MCP are host-plane and preset-independent: callable under every preset.",
740
924
  "compat.hint.doctor": "Command-line check:",
741
- "scenes.title": "Scenes", "scenes.desc": "Manage scenes: preset different usage scenes combining MCP servers, skills, subagents and memories",
925
+ "scenes.title": "Scenes", "scenes.desc": "Manage scenes: preset combinations of MCP, skills, subagents and memories, switch with one click.",
742
926
  "scenes.stat.total": "scene(s)", "scenes.stat.active": "enabled", "scenes.stat.archives": "with a profile",
743
927
  "scenes.noDesc": "No description yet",
744
928
  "scenes.mode.active": "Active mode: {name}", "scenes.mode.profile": "Profile: {parts}", "scenes.mode.noProfile": "This scene has no profile yet",
745
- "scenes.mode.enter": "Enter this mode", "scenes.empty": "No dedicated scenes yet — global memories are injected without one; create a scene when you need to switch MCP servers, skills or personas.",
929
+ "scenes.empty": "No dedicated scenes yet — global memories are injected without one; create a scene when you need to switch MCP servers, skills or personas.",
746
930
  "scenes.field.desc.limit": "Up to {count} characters; longer text is clipped on the card.",
747
931
  "scenes.profile.mcp": "{count} MCP", "scenes.profile.skills": "{count} skill(s)", "scenes.profile.subagents": "{count} subagent(s)", "scenes.profile.memories": "{count} memor(ies)",
748
932
  "scenes.mcp.hint": "Add the MCP section first, then check the servers to enable (stopped ones included); use Pick tools to narrow to specific tools.",
749
933
  "scenes.seg.selectAll": "Select all", "scenes.seg.clear": "Clear", "scenes.seg.checked": "{checked}/{total} selected",
750
934
  "scenes.archive.sectionOff": "not defined",
751
- "scenes.archive.summary": "Profile: {mcp} MCP server(s) · {skills} skill(s) · {subagents} subagent(s) · {memories} memory item(s)",
752
- "scenes.archive.note": "A newly added section starts with nothing checked. An undefined section leaves that domain untouched; a defined-but-empty section disables that domain (the subagent section is the exception: empty means no restriction; the memory section only affects injection — no file is deleted).",
935
+ "scenes.archive.summary": "Profile: {mcp} MCP server(s) · {skills} skill(s) · {subagents} subagent(s)",
936
+ "scenes.archive.note": "An undefined section leaves that domain untouched; a defined section switches both ways: checked items are enabled, unchecked ones disabled. MCP really starts/stops the server process; skills toggle the source they live in.",
753
937
  "scenes.mcp.noServers": "No MCP servers to pick from yet", "scenes.mcp.toolCount": "{count} tool(s)", "scenes.skills.empty": "No skills to pick from yet",
754
938
  "scenes.mcp.allTools": "all tools", "scenes.mcp.pickedCount": "{count} tools picked", "scenes.mcp.notRunning": "not running", "scenes.mcp.pickTools": "Pick tools",
755
939
  "scenes.mcp.toolsOf": "Tool details", "scenes.mcp.back": "Back", "scenes.mcp.noTools": "No tools listed for this server (not running or no tools)", "scenes.mcp.drillHint": "Checked = enabled in this scene; unchecked = disabled. A whole-server check defaults to all tools.",
756
940
  "scenes.skills.hint": "Add the skills section first, then check the skills enabled in this scene.", "scenes.subagents.hint": "Add the subagent section first, then check the personas callable in this scene.", "scenes.subagents.empty": "No personas yet — create one on the Subagents page.",
757
- "scenes.filter.skills": "Filter skills (name)", "scenes.filter.subagents": "Filter personas (name or description)", "scenes.filter.servers": "Filter servers",
758
- "scenes.mem.hint": "Add the memory section, then check the memories injected for this scene (only this scene's own memories are listed — global memories are always injected and need no configuration here); unchecked memories never reach the system prompt (their files stay).",
941
+ "scenes.filter.skills": "Filter skills / directories", "scenes.filter.subagents": "Filter personas (name or description)", "scenes.filter.servers": "Filter servers",
942
+
759
943
  "scenes.mem.title": "Memories", "scenes.mem.search": "Filter memories in this scene (name or description)",
760
944
  "scenes.mem.noMatch": "Nothing matches", "scenes.mem.emptyScene": "This scene has no memories yet",
761
945
  "memory.mode.current": "Active mode", "memory.mode.exit": "Exit mode",
762
946
  "memory.archive.edit": "Profile", "memory.archive.title": "Scene profile",
763
947
  "memory.archive.tools": "MCP tools", "memory.archive.skills": "Skills", "memory.archive.subagents": "Subagent binding", "memory.archive.memories": "Memories",
764
- "memory.archive.addTools": "+ Tools", "memory.archive.addSkills": "+ Skills", "memory.archive.addSubagents": "+ Subagents", "memory.archive.addMemories": "+ Memories",
948
+ "memory.archive.addTools": "+ Tools", "memory.archive.addSkills": "+ Skills", "memory.archive.addSubagents": "+ Subagents",
765
949
  "memory.archive.removeSection": "Remove section", "memory.archive.save": "Save to scene",
766
950
  "memory.archive.emptySection": "Section defined with nothing checked = all disabled", "memory.archive.emptySubagents": "Section defined with nothing checked = no restriction (all personas allowed)", "memory.archive.stale": "Stale entries (no longer exist, skipped): {items}",
767
- "memory.result.archiveSaved": "Scene profile saved: {name}", "memory.result.modeSet": "Entered mode: {name}", "memory.result.modeExited": "Exited mode",
768
- "memory.desc": "Manage scene memories: memories in enabled scenes are injected into the system prompt automatically, and a blank scene means global injection.",
951
+ "memory.result.archiveSaved": "Scene profile saved: {name}", "memory.result.modeSet": "Entered mode: {name}", "memory.result.modeSwitched": "{mcp} server(s) / {skills} source(s) switched", "memory.result.modeExited": "Exited mode",
952
+ "memory.desc": "Manage memories: enabled memories enter the system prompt; enable them per scene.",
769
953
  "memory.btn.refresh": "Refresh", "memory.btn.new": "New memory", "memory.btn.create": "Create", "memory.btn.save": "Save",
770
954
  "memory.btn.diagnose": "Check", "memory.btn.delete.confirm": "Move to trash",
771
955
  "memory.diagnose.title": "Memory checkup", "memory.diagnose.summary": "{rules} memories · {scenes} scenes · {issues} issues", "memory.diagnose.clean": "No issues found.",
@@ -779,10 +963,10 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
779
963
  "memory.scene.global": "Global", "memory.scene.count": "{count} memories", "memory.scene.new": "New memory", "memory.scene.browse": "Pick existing",
780
964
  "memory.scene.empty": "No memories in this scene yet",
781
965
  "memory.scene.orphan": "No scene", "memory.scene.orphan.tag": "never injected", "memory.scene.orphan.hint": "These memories sit directly in the memories/ root and belong to no scene, so they never reach the system prompt. Move them into a scene directory (or into memories/global/ to make them “Global”).",
782
- "memory.scene.edit": "Edit", "memory.scene.editTitle": "Edit scene", "memory.scene.editHelp": "Only the display name and description change; memory files are untouched.",
966
+ "memory.scene.edit": "Edit", "memory.scene.editTitle": "Edit scene", "memory.scene.editHelp": "The name is its memory folder name; renaming moves the folder, profile and bindings along. Memory files stay untouched.",
783
967
  "memory.scene.field.name": "Scene name", "memory.scene.field.name.placeholder": "e.g. office",
784
- "memory.scene.field.name.hint": "Cannot be renamed after creation.",
785
- "memory.scene.field.name.lock": "The scene name cannot be changed after creation",
968
+ "memory.scene.field.name.hint": "The name is its top-level folder name; you can still change it later.",
969
+ "memory.scene.field.name.lock": "Renaming moves its memory folder, scene profile and enable set along; memory files stay untouched.",
786
970
  "memory.scene.field.desc": "Description (optional)", "memory.scene.field.desc.placeholder": "One line on what this scene is for",
787
971
  "memory.search.placeholder": "Search name / description / scene", "memory.filter.all": "All scenes",
788
972
  "memory.empty": "No memories yet — click “New memory”", "memory.empty.search": "No matching memories",
@@ -808,41 +992,50 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
808
992
  "memory.trash.confirmTitle": "Delete forever?", "memory.trash.confirmDesc": "“{name}” is deleted forever and cannot be recovered",
809
993
  "memory.btn.newScene": "New scene", "memory.btn.deleteScene": "Delete scene", "memory.btn.saveScene": "Save changes",
810
994
  "memory.scene.createTitle": "New scene",
811
- "memory.deleteScene.title": "Delete scene?", "memory.deleteScene.desc": "Deletes the scene record and its empty folder “{name}” (refused while it still holds memories or is the active mode; “Global” is reserved and cannot be deleted)",
812
- "memory.result.sceneCreated": "Scene created: {name}", "memory.result.sceneUpdated": "Scene saved: {name}", "memory.result.sceneRemoved": "Scene deleted: {name}",
995
+ "memory.deleteScene.title": "Delete scene?", "memory.deleteScene.desc": "Moves “{name}” and every memory under it (including disabled ones) to the trash, restorable as one entry; a scene in use cannot be deleted.",
996
+ "memory.result.sceneCreated": "Scene created: {name}", "memory.result.sceneUpdated": "Scene saved: {name}", "memory.result.sceneRemoved": "Scene deleted: {name}", "memory.result.sceneRemoved.withMemories": "Scene deleted: {name} (with {count} memory file(s); restorable as one entry from the trash)",
813
997
  "memory.table.name": "Memory name and description", "memory.table.tags": "Tags", "memory.table.status": "Status",
814
- "error.rules.invalidArgs": "Missing arguments: pass all:true (enable everything) or scenes:[...] (explicit narrowing)", "error.rules.invalidGroup": "Invalid scene/group name (non-empty, ≤64 chars, no path separators or < > : \" | ? *, must not start with a dot)", "error.rules.invalidName": "Invalid memory name (non-empty, ≤64 chars, no / \\ < > : \" | ? *, must not start with a dot)", "error.rules.descriptionRequired": "Description is required", "error.rules.descriptionTooLong": "Description is too long (max 500 chars)", "error.rules.bodyRequired": "Body is required", "error.rules.tooLarge": "Rule content is too large", "error.rules.shadowed": "Rule is shadowed by a bundle of the same name", "error.rules.notFound": "Rule not found", "error.rules.budgetExceeded": "Scene memory section exceeds its budget", "error.rules.ioFailed": "File operation failed", "error.rules.sceneNotEmpty": "Scene is not empty; cannot be deleted", "error.rules.notBundle": "This memory is flat (a single file) and cannot carry attachments", "error.rules.noFiles": "No files selected", "error.rules.emptyFile": "Empty file", "error.rules.fileTooLarge": "File too large (max {limit} MB each)", "error.rules.tooManyFiles": "At most {limit} files per upload", "error.rules.sceneInMode": "This scene is the active mode; exit the mode before deleting it",
815
- "subagents.title": "Subagents", "subagents.desc": "Manage personas: one persona per file, its body is the subagent's system prompt.",
816
- "prompts.desc": "Manage the global instruction baseline: applying writes AGENTS.md; it takes effect in new sessions.",
998
+ "error.rules.invalidArgs": "Missing arguments: pass all:true (enable everything) or scenes:[...] (explicit narrowing)", "error.rules.invalidGroup": "Invalid scene/group name (non-empty, ≤64 chars, no path separators or < > : \" | ? *, must not start with a dot)", "error.rules.invalidName": "Invalid memory name (non-empty, ≤64 chars, no / \\ < > : \" | ? *, must not start with a dot)", "error.rules.descriptionRequired": "Description is required", "error.rules.descriptionTooLong": "Description is too long (max 500 chars)", "error.rules.bodyRequired": "Body is required", "error.rules.tooLarge": "Rule content is too large", "error.rules.shadowed": "Rule is shadowed by a bundle of the same name", "error.rules.notFound": "Rule not found", "error.rules.budgetExceeded": "Scene memory section exceeds its budget", "error.rules.ioFailed": "File operation failed", "error.rules.nameTaken": "That name is already taken", "error.rules.sceneNotEmpty": "Scene is not empty; cannot be deleted", "error.rules.notBundle": "This memory is flat (a single file) and cannot carry attachments", "error.rules.noFiles": "No files selected", "error.rules.emptyFile": "Empty file", "error.rules.fileTooLarge": "File too large (max {limit} MB each)", "error.rules.tooManyFiles": "At most {limit} files per upload", "error.rules.sceneInMode": "This scene is the active mode; exit the mode before deleting it",
999
+ "subagents.title": "Subagents", "subagents.stat.total": " subagents", "subagents.stat.limited": " tool-restricted", "subagents.desc": "Manage subagents: create and import personas; a persona's body is the subagent's system prompt.",
1000
+ "prompts.desc": "Manage prompts: preset the global instruction baseline; applying writes AGENTS.md and takes effect on the next turn.",
817
1001
  "sessions.desc": "Manage archived sessions: restore or delete them permanently; expired ones are cleaned up automatically.",
818
- "subagents.new": "New persona", "subagents.empty": "No personas yet — click “New persona” to create the first one.",
1002
+ "subagents.new": "New subagent", "subagents.empty": "No subagents yet — click “New subagent” to create the first one.",
819
1003
  "subagents.create": "New persona", "subagents.edit": "Edit persona",
820
1004
  "subagents.field.name": "Persona name",
821
1005
  "subagents.field.description": "Description", "subagents.field.description.placeholder": "e.g. Senior Java engineer",
822
1006
  "subagents.field.model": "Model", "subagents.field.model.placeholder": "Custom model id", "subagents.field.model.hint": "Blank inherits the main session", "subagents.field.provider": "Model provider", "subagents.field.provider.placeholder": "sensenova", "subagents.field.provider.hint": "Provider and model are a pair: a model alone resolves against the main session's provider and fails across providers.",
823
1007
  "subagents.model.inherit": "Inherit the main session (unspecified)", "subagents.model.customOption": "Custom / not in the catalogue…",
824
- "subagents.field.tools": "Tool allowlist", "subagents.field.tools.hint": "Checked = the subagent keeps only these tools; nothing checked = no restriction (the base tool set).",
825
- "subagents.field.toolsDeny": "Tool denylist", "subagents.field.toolsDeny.hint": "Checked = remove these from the subagent's visible tools (takes precedence over the allowlist). Use it for “everything except bash” style setups.",
826
- "subagents.tools.chosen": "Nothing selected = no restriction", "subagents.tools.filter": "Filter tool names", "subagents.tools.empty": "The host returned no selectable tools (the model may not support catalogue enumeration)",
827
- "subagents.tools.unavailable": "not visible here", "subagents.tools.current": "available now", "subagents.tools.groupCurrent": "Available in this session", "subagents.tools.groupOther": "Available in other agent presets",
828
- "subagents.tools.count": "{count} selected", "subagents.tools.remove": "Remove",
829
- "subagents.adv.title": "Advanced options", "subagents.adv.inherit": "inherit", "subagents.adv.summary": "model {model} · allow {allow} · deny {deny}", "subagents.adv.note": "Tools are grouped by official Agent preset; the same tool may belong to multiple presets and only applies under the matching preset.", "subagents.adv.loadFailed": "Could not read the candidate data (model catalogue / tool list); you can still fill the fields by hand.",
1008
+ "subagents.field.modes": "Tool limits (per agent preset)",
1009
+ "subagents.field.modes.hint": "Only the row for the current preset applies; other presets keep their full tool set.",
1010
+ "subagents.mode.startAllow": "Enable allowlist", "subagents.mode.startDeny": "Enable denylist", "subagents.mode.stopAllow": "Disable allowlist", "subagents.mode.stopDeny": "Disable denylist",
1011
+ "subagents.mode.off": "no restriction", "subagents.mode.empty": "enabled with nothing picked (= no restriction)",
1012
+ "subagents.mode.allowOn": "allowlist {count}", "subagents.mode.denyOn": "denylist {count}",
1013
+ "subagents.mode.allowEmpty": "Nothing picked = no restriction (every tool of this preset stays available)",
1014
+ "subagents.mode.denyEmpty": "Nothing picked = no restriction (every tool of this preset stays available)",
1015
+ "subagents.mode.noTools": "Could not read this preset's tool list",
1016
+ "subagents.mode.broken": "This preset is broken; its tool list is unreadable",
1017
+ "subagents.mode.noPresets": "The host returned no agent-preset roster",
1018
+ "subagents.legacy.note": "This file still carries the old-style tool limits (allowlist {allow} / denylist {deny}); they apply to every preset.",
1019
+ "subagents.legacy.convert": "Convert into a preset row",
1020
+ "subagents.legacy.hint": "Converting moves the old list into the preset you pick; until then the old keys stay untouched in the file (and keep working at run time).",
1021
+ "subagents.tools.filter": "Filter tool names", "subagents.tools.remove": "Remove",
1022
+ "subagents.adv.title": "Advanced options", "subagents.adv.inherit": "inherit", "subagents.adv.summary": "model {model} · preset limits {modes} · legacy allow {allow} / deny {deny}", "subagents.adv.note": "Allowlist = the subagent may only use the picked tools; denylist = everything except the picked ones. MCP tools are not listed: a subagent always keeps the MCP servers currently running.", "subagents.adv.loadFailed": "Could not read the candidate data (model catalogue / tool list); you can still fill the fields by hand.",
1023
+ "preset.name.standard": "Standard mode", "preset.name.ptc": "PTC mode", "preset.name.minimal": "Minimal mode", "preset.name.cordis": "Creator mode",
1024
+ "preset.short.standard": "Standard", "preset.short.ptc": "PTC", "preset.short.minimal": "Minimal", "preset.short.cordis": "Creator",
830
1025
  "subagents.field.body": "Persona prompt", "subagents.field.body.placeholder": "Describe the persona's role, responsibilities, and working style…",
831
1026
  "subagents.result.saved": "Persona saved: {name}", "subagents.result.deleted": "Persona deleted: {name}",
832
- "subagents.delete.title": "Delete persona?", "subagents.delete.desc": "Deletes the persona file “{name}”; this cannot be undone",
833
- "subagents.import": "Import", "subagents.import.title": "Import personas", "subagents.import.hint": "Accepts .md or .zip (multi-select or drag in): one .md = one persona, the file name is the persona name; existing names are skipped.",
1027
+ "subagents.delete.title": "Delete persona?", "subagents.delete.desc": "Moves the persona file “{name}” to the trash (restorable from the Trash button on the Subagents page).",
1028
+ "subagents.import": "Import subagents", "subagents.import.title": "Import subagents", "subagents.toggle": "Toggle subagent", "subagents.disabled": "Disabled", "subagents.disabled.hint": "Not injected into the system prompt and hidden from subagent_list / subagent_run; the file is kept and can be re-enabled anytime.",
834
1029
  "subagents.result.imported": "Imported {count} persona(s): {names}",
835
1030
  "memory.import": "Import memories", "memory.import.title": "Import memories", "memory.import.scene": "Import into scene",
836
- "memory.import.sceneHint": "Blank = global; directory names inside the zip become scene names.",
837
- "memory.import.hint": "Accepts .md or .zip (multi-select or drag in): one .md = one memory; <scene>/<name>/SKILL.md inside a zip imports as a bundle with sibling files as attachments; existing names are skipped.",
838
1031
  "memory.result.imported": "Imported {count} memory/memories: {names}",
839
1032
  "memory.import.scenesCreated": "Scenes created for them: {names}",
840
1033
  "memory.scene.global.hint": "Global",
841
- "import.pick": "Choose files", "import.selected": "{count} file(s) selected", "import.clear": "Clear", "import.submit": "Install",
1034
+ "import.clear": "Clear", "import.submit": "Import",
842
1035
  "import.none": "Nothing was imported (all skipped)", "import.skipped": "Skipped: {items}",
843
1036
  "error.import.noFiles": "No files selected",
844
1037
  "nav.title": "Tools",
845
- "agm.current.external": "(edited externally)", "agm.current.none": "(not set)", "agm.current.label": "Current: {label}", "agm.loading": "Loading…", "agm.empty": "No presets yet — click “New preset” to create one", "agm.btn.delete.blocked": "The preset currently in effect cannot be deleted; enable another scene, rebind the prompt, or apply a different preset first.", "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.current.byScene": "{id} (scene “{scene}”)", "agm.apply.hint": "Takes effect in new sessions; the current session is unchanged", "agm.btn.reapply": "Apply again", "agm.btn.apply": "Apply", "agm.applyModal.title": "Apply preset", "agm.edit.title": "Edit preset", "agm.remove.title": "Delete preset", "agm.btn.edit": "Edit", "agm.btn.delete": "Delete", "agm.btn.new": "New preset", "agm.btn.import": "Import", "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": "Any characters are fine (spaces, dots, non-ASCII); no / \\ < > : \" | ? *, must not start with a dot, must not end with a dot or space, ≤64 chars. Changing it renames the directory and follows scene bindings.", "agm.field.id.placeholder": "e.g. work-baseline", "agm.field.content": "Content", "agm.field.content.placeholder": "Write the preset body (Markdown) here; leave blank to start from the empty template", "agm.field.copyFrom": "Or copy from an existing preset", "agm.field.copyFrom.none": "Do not copy (empty template)", "agm.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 in new sessions; 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": "Workspace removed", "hist.group.removed.hint": "This directory's workspace registration was deleted; the group is rebuilt from session directories and re-joins automatically once registered again", "hist.group.unregistered": "Unregistered directory", "hist.group.unregistered.hint": "This directory currently has no DSH workspace registration; the group is rebuilt from session directories", "hist.btn.registerWs": "Register as workspace", "hist.btn.registerWs.hint": "Register this directory as a workspace 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 as workspace?", "hist.register.body": "A workspace registration will be created in DSH for “{title}”, and the sessions under this directory will be attached back to it (restoring their grouping). This writes workspace accounting only — no file or session is moved or deleted.", "hist.register.confirm": "Register", "hist.register.done.title": "Registered", "hist.register.done.body": "Workspace “{title}” is registered; archived sessions under this directory join that group after a refresh.", "hist.register.done.attached": "Attached {count} existing session(s) under this directory back to the workspace.", "hist.register.done.skipped": "{count} session(s) could not be attached: {reason}", "hist.err.register": "Registration failed", "scenes.field.prompt": "Prompt preset", "scenes.agents.note.applied": "wrote “{id}” into AGENTS.md", "scenes.agents.note.restored": "restored the AGENTS.md baseline from before the scene", "scenes.agents.note.error": "AGENTS.md was not written: {reason}", "scenes.field.prompt.hint": "While this scene is enabled, its preset is the active global baseline. Defaults to the one currently in effect.", "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.enable.blocked": "Scene “{name}” is already enabled; turn it off first", "scenes.result.created.collapsed": "Scene “{name}” created and left off (the legacy “all enabled” default collapsed to a single choice)", "scenes.legacyAll": "{count} scenes are enabled at once (a leftover of the legacy “all enabled” default); only one scene besides “Global” may be enabled.", "scenes.legacyAll.fix": "Collapse to one", "agm.scene.tag": "In use by scene “{scene}”", "agm.scene.tag.hint": "Bound by that scene; takes effect with the scene switch", "agm.scene.missing": "Bound by scene “{scene}”, but the file is missing", "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": "Clear selection", "hist.btn.import": "Import conversation", "hist.btn.export": "Export conversations", "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.selected": " selected", "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.hint": "Import transcripts from other agents to create a new, continuable session", "hist.import.cwd": "Project directory (optional)", "hist.import.cwd.placeholder": "Absolute path, optional (the session goes to Ungrouped)", "hist.import.drop": "Choose or drop a conversation file", "hist.import.done": "Created session {id} ({count} messages); continue it from the DSH session list.", "hist.export.hint": "Export the selected sessions as transcript files that can be imported again", "hist.export.dir": "Export directory (absolute path, created automatically)", "hist.export.browse": "Browse and choose a folder", "hist.btn.browse": "Choose", "hist.export.pickDir": "Choose export directory", "hist.export.format": "Format", "hist.export.scope": "Session scope", "hist.export.workspace": "Workspace", "hist.export.sessions": "Sessions ({count})", "hist.export.loading": "Loading sessions…", "hist.cwd.missing": "⚠ The workspace directory no longer exists: ", "hist.tag.archived": "Archived", "hist.tag.live": "Not archived", "hist.export.empty": "No matching sessions", "hist.result.exported": "Exported {count} session(s) to {dir}", "hist.result.skipped": "; skipped {count} ({ids})", "hist.result.archived": "Archived {count} session(s) to History; manage them on the History page", "hist.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",
1038
+ "agm.stat.total": " prompts", "agm.stat.active": " active", "agm.loading": "Loading…", "agm.empty": "No prompts yet — click “New prompt” to create one", "agm.btn.delete.blocked": "The preset currently in effect cannot be deleted; enable another scene, rebind the prompt, or apply a different preset first.", "agm.active": "Active", "agm.active.hint.scene": "Currently active: the prompt preset bound to scene “{scene}”", "agm.active.hint.file": "Currently active: AGENTS.md is exactly this preset", "agm.file.applied": "In AGENTS.md", "agm.file.applied.hint": "AGENTS.md currently holds this one; the enabled scene binds a different preset, which now takes effect", "agm.apply.hint": "Writes AGENTS.md; the host re-reads that file every turn, so it takes effect on the next turn", "agm.btn.reapply": "Apply again", "agm.btn.apply": "Apply", "agm.applyModal.title": "Apply preset", "agm.edit.title": "Edit preset", "agm.remove.title": "Delete preset", "agm.btn.edit": "Edit", "agm.btn.delete": "Delete", "agm.btn.new": "New prompt", "agm.btn.import": "Import prompts", "agm.result.imported": "Imported {count} preset(s): {names}", "agm.btn.save": "Save", "agm.btn.create": "Create", "agm.btn.confirmRemove": "Delete", "agm.btn.confirmApply": "Apply", "agm.field.id": "id (directory name)", "agm.field.id.hint": "Letters, spaces and dots are fine; no slashes or : * ? < >; up to 64 chars. Changing it renames the directory.", "agm.field.id.placeholder": "e.g. work-baseline", "agm.field.content": "Content", "agm.field.content.placeholder": "Write the preset body (Markdown) here; leave blank to start from the empty template", "agm.field.copyFrom": "Or copy from an existing preset", "agm.field.copyFrom.none": "Do not copy (empty template)", "agm.field.desc": "Description (for humans only)", "agm.field.desc.placeholder": "What this preset is for and when to use it", "agm.result.renamed": "Saved; id is now “{id}”", "agm.result.rebound": " ({count} scene binding(s) updated)", "agm.remove.suffix": "? This cannot be undone.", "agm.apply.prefix": "Write the contents of ", "agm.apply.suffix": " into ~/.dsh/AGENTS.md.", "agm.apply.note": "Takes effect on the next turn; the current content is backed up to __last-applied__ (last 5 kept)", "hist.err.load": "Load failed", "hist.err.unarchive": "Restore failed", "hist.err.delete": "Delete failed", "hist.err.retention": "Update failed", "hist.err.import": "Import failed", "hist.err.sessions": "Failed to load sessions", "hist.err.export": "Export failed", "hist.err.archive": "Archive failed", "hist.retention.forever": "Keep forever", "hist.retention.days": "{count} days", "hist.arch.all": "All sessions", "hist.arch.archived": "Archived only", "hist.arch.live": "Not archived only", "hist.arch.missing": "Missing directory only", "hist.ws.all": "All workspaces", "hist.ungrouped": "Ungrouped", "hist.group.removed": "Removed", "hist.group.removed.hint": "This directory has no DSH workspace registration right now; the group is rebuilt from session directories and rejoins automatically once registered again", "hist.btn.registerWs": "Register", "hist.btn.registerWs.hint": "Register this directory again and attach its existing sessions back to that group (workspace accounting only — no file or session is moved or deleted)", "hist.register.title": "Register again?", "hist.register.body": "A workspace registration will be created in DSH for “{title}”, and the sessions under this directory will be attached back to it (restoring their grouping). This writes workspace accounting only — no file or session is moved or deleted.", "hist.register.confirm": "Register", "hist.register.done.title": "Registered", "hist.register.done.body": "Workspace “{title}” is registered; archived sessions under this directory join that group after a refresh.", "hist.register.done.attached": "Attached {count} existing session(s) under this directory back to the workspace.", "hist.register.done.skipped": "{count} session(s) could not be attached: {reason}", "hist.err.register": "Registration failed", "scenes.field.prompt": "Prompt preset", "scenes.agents.note.applied": "wrote “{id}” into AGENTS.md", "scenes.agents.note.restored": "restored the AGENTS.md baseline from before the scene", "scenes.agents.note.error": "AGENTS.md was not written: {reason}", "scenes.field.prompt.hint": "While this scene is enabled, its preset is the active global baseline. Defaults to the one currently in effect.", "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.enable.blocked": "Scene “{name}” is already enabled; turn it off first", "scenes.result.created.collapsed": "Scene “{name}” created and left off (the legacy “all enabled” default collapsed to a single choice)", "scenes.legacyAll": "{count} scenes are enabled at once (a leftover of the legacy “all enabled” default); only one scene besides “Global” may be enabled.", "scenes.legacyAll.fix": "Collapse to one", "agm.scene.tag": "In use by scene “{scene}”", "agm.scene.tag.hint": "Bound by that scene; takes effect with the scene switch", "agm.scene.missing": "Bound by scene “{scene}”, but the file is missing", "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": "Clear selection", "hist.btn.import": "Import sessions", "hist.btn.export": "Export sessions", "hist.selected": "{count} selected", "hist.btn.restoreSelected": "Restore selected ({count})", "hist.btn.deleteSelected": "Delete selected ({count})", "hist.stat.archived": " archived", "hist.stat.projects": " projects", "hist.stat.retention": " days kept", "hist.search.placeholder": "Search title / session id / project path", "hist.loading": "Loading…", "hist.empty.search": "No matching archived sessions", "hist.empty.none": "No archived sessions yet", "hist.import.cwd": "Project directory (optional)", "hist.import.cwd.placeholder": "Absolute path, optional (the session goes to Ungrouped)", "hist.import.drop": "Choose or drop a conversation file", "hist.import.done": "Created session {id} ({count} messages); continue it from the DSH session list.", "hist.export.hint": "Export the selected sessions as transcript files that can be imported again", "hist.export.dir": "Export directory (absolute path, created automatically)", "hist.export.browse": "Browse and choose a folder", "hist.btn.browse": "Choose", "hist.export.pickDir": "Choose export directory", "hist.export.format": "Format", "hist.export.scope": "Session scope", "hist.export.workspace": "Workspace", "hist.export.sessions": "Sessions ({count})", "hist.export.loading": "Loading sessions…", "hist.cwd.missing": "⚠ The workspace directory no longer exists: ", "hist.tag.archived": "Archived", "hist.tag.live": "Not archived", "hist.export.empty": "No matching sessions", "hist.result.exported": "Exported {count} session(s) to {dir}", "hist.result.skipped": "; skipped {count} ({ids})", "hist.result.archived": "Archived {count} session(s) to History; manage them on the History page", "hist.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",
846
1039
  }
847
1040
  };
848
1041
 
@@ -900,14 +1093,14 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
900
1093
  ? { kind: 'warn', text: mt('mcp.msg.warn', { warning: res.warning }) }
901
1094
  : { kind: 'ok', text: mt('mcp.msg.ok') }
902
1095
  )
903
- const emptyForm = () => ({ serverName: '', transport: 'streamable-http', url: '', command: '', args: '', headers: '', env: '', level: 'project' })
1096
+ const emptyForm = () => ({ serverName: '', transport: 'streamable-http', url: '', command: '', args: '', headers: '', env: '', level: 'global' })
904
1097
  const kvToLines = (obj) => (obj ? Object.keys(obj).map((k) => k + '=' + obj[k]).join('\n') : '')
905
1098
  // 级别筛选选项:必须在**渲染时**取词。原来写成模块级常量,apply() 早于 locale
906
1099
  // 注册,导致英文界面下这四项永远显示中文(且再也翻不过来)。
907
1100
  const mcpLevelOptions = () => [
908
1101
  { value: '', label: mt('mcp.level.all') },
909
- { value: 'project', label: mt('mcp.level.project') },
910
1102
  { value: 'global', label: mt('mcp.level.global') },
1103
+ { value: 'project', label: mt('mcp.level.project') },
911
1104
  { value: 'loader', label: mt('mcp.level.loader') },
912
1105
  ]
913
1106
  const MCP_TRANSPORT_OPTIONS = [
@@ -919,16 +1112,17 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
919
1112
  return s.length <= 4 ? '****' : s.slice(0, 4) + '****'
920
1113
  }
921
1114
 
922
- // 顶部栏反馈入口:合并成一个「GitHub 和反馈」链接,指向仓库(issues 在那)。
1115
+ // 顶部栏右端:GitHub 链接(issues 在仓库)+ 版本号(全界面唯一显示处)。
923
1116
  function FeedbackLinks() {
924
1117
  return React.createElement('div', { className: 'dsm-feedback-links' },
925
1118
  React.createElement('a', { className: 'dsm-feedback-link', href: 'https://github.com/ouli-1242/dsh-plugin-tool-management', target: '_blank', rel: 'noreferrer', 'aria-label': t('link.project'), title: t('link.project') },
926
- React.createElement(GithubMark16, null)))
1119
+ React.createElement(GithubMark16, null)),
1120
+ React.createElement(VersionBadgeComponent, null))
927
1121
  }
928
1122
 
929
1123
  // MCP 服务页:与 Skills 页共用 dsm-* 设计语言(统计卡 / 筛选 / 分组卡片 / 模态框)。
930
1124
  function MCPPage() {
931
- const [state, setState] = React.useState({ loading: true, error: null, rows: [], paths: null, errors: [], warnings: [] })
1125
+ const [state, setState] = React.useState({ loading: true, error: null, rows: [], paths: null, errors: [], warnings: [], revealError: null })
932
1126
  const [msg, setMsg] = React.useState(null)
933
1127
  const [busy, setBusy] = React.useState(null)
934
1128
  const [query, setQuery] = React.useState('')
@@ -941,13 +1135,15 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
941
1135
  const [reveal, setReveal] = React.useState(false)
942
1136
  const [settings, setSettings] = React.useState(null)
943
1137
  const [noteDraft, setNoteDraft] = React.useState('')
1138
+ const [tokenDraft, setTokenDraft] = React.useState('')
1139
+ const [compactConfirm, setCompactConfirm] = React.useState(false)
944
1140
  const [, setTick] = React.useState(0)
945
1141
 
946
1142
  const refresh = (withReveal, onRows) => {
947
1143
  const useReveal = withReveal === undefined ? reveal : withReveal === true
948
- // 明文视图走独立的 mcpm-reveal op(host 端受 token 保护),
949
- // mcpm-list 始终脱敏,无 token 也能渲染页面。
950
- apiCall(useReveal ? 'mcpm-reveal' : 'mcpm-list', {}).then((res) => {
1144
+ // 明文视图走独立的 mcpm-reveal op(host 端要求带对的访问令牌),
1145
+ // mcpm-list 始终脱敏、无令牌也能渲染页面。
1146
+ const applyRows = (res, revealError) => {
951
1147
  const rows = (res && res.rows) || []
952
1148
  setState({
953
1149
  loading: false,
@@ -956,9 +1152,20 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
956
1152
  paths: (res && res.paths) || null,
957
1153
  errors: (res && res.errors) || [],
958
1154
  warnings: (res && res.warnings) || [],
1155
+ revealError: revealError || null,
1156
+ locked: (res && res.anyLocked) === true,
959
1157
  })
960
1158
  if (onRows) onRows(rows)
961
- }).catch((e) => setState({ loading: false, error: String((e && e.message) || e), rows: [], paths: null, errors: [], warnings: [] }))
1159
+ }
1160
+ apiCall(useReveal ? 'mcpm-reveal' : 'mcpm-list', {}).then((res) => {
1161
+ // 明文被拒(未配令牌 / 令牌不对):退回脱敏列表,把原因与令牌入口单独显示。
1162
+ // 直接落进 state.error 会把整页变成错误态 —— "看不了明文"不等于"页面坏了"。
1163
+ if (useReveal && res && res.ok === false) {
1164
+ setReveal(false)
1165
+ return apiCall('mcpm-list', {}).then((masked) => applyRows(masked, { code: res.code, text: res.error }))
1166
+ }
1167
+ applyRows(res, null)
1168
+ }).catch((e) => setState({ loading: false, error: String((e && e.message) || e), rows: [], paths: null, errors: [], warnings: [], revealError: null, locked: false }))
962
1169
  }
963
1170
  // The polling tick must always call the newest refresh closure, while
964
1171
  // the interval itself only changes when the saved settings change.
@@ -1063,6 +1270,17 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1063
1270
  if (fresh) setDetail(Object.assign({}, detail, { row: fresh }))
1064
1271
  })
1065
1272
  }
1273
+ // 明文被拒后的令牌入口:填对令牌 → 存 localStorage(随每个请求以 x-dsh-token 发出)
1274
+ // → 重试明文。宿主没配令牌时这一步不会成功,面板里的提示写了该怎么配。
1275
+ const applyToken = () => {
1276
+ if (!setAccessToken(tokenDraft)) return
1277
+ setReveal(true)
1278
+ refresh(true, (rows) => {
1279
+ if (!detail) return
1280
+ const fresh = rows.find((item) => item.id === detail.row.id && item.level === detail.row.level)
1281
+ if (fresh) setDetail(Object.assign({}, detail, { row: fresh }))
1282
+ })
1283
+ }
1066
1284
  const saveNote = () => {
1067
1285
  const row = detail && detail.row
1068
1286
  if (!row) return
@@ -1080,6 +1298,20 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1080
1298
  setConfirmRow(null)
1081
1299
  if (row) run('mcpm-remove', { id: row.id, level: row.level }, row.id + ':remove')
1082
1300
  }
1301
+ // 「整理补丁」:删掉多余的启停覆盖块(生效值不变,只删被盖住的块与纯 no-op)。
1302
+ // 成功文案带真实删除数量;一条都没删时如实说"已经是干净的",而不是笼统的"成功"。
1303
+ const doCompact = () => {
1304
+ setCompactConfirm(false)
1305
+ setMsg(null)
1306
+ setBusy('compact')
1307
+ apiCall('mcpm-compact', {}).then((res) => {
1308
+ if (res && res.ok) {
1309
+ const n = Number(res.removed || 0)
1310
+ setMsg({ kind: 'ok', text: n > 0 ? mt('mcp.compact.done', { count: n }) : mt('mcp.compact.clean') })
1311
+ refresh()
1312
+ } else setMsg({ kind: 'err', text: (res && res.error) || mt('mcp.msg.failed') })
1313
+ }).catch((e) => setMsg({ kind: 'err', text: String((e && e.message) || e) })).then(() => setBusy(null))
1314
+ }
1083
1315
  const openDetail = (row) => {
1084
1316
  setNoteDraft(row.notes || '')
1085
1317
  setDetail({ row, loading: true, error: null, tools: [] })
@@ -1144,7 +1376,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1144
1376
  const renderRow = (row) => {
1145
1377
  const status = liveStatus(row)
1146
1378
  const hint = liveHint(row)
1147
- const editable = row.level !== 'loader'
1379
+ // 场景锁定期间整页只读(开关/编辑/删除一并消失,重启也归入冻结)。
1380
+ const editable = row.level !== 'loader' && state.locked !== true
1148
1381
  return React.createElement('div', { key: row.id, className: 'dsm-row' },
1149
1382
  React.createElement('div', { className: 'dsm-main' },
1150
1383
  React.createElement('div', { className: 'dsm-name' }, row.serverName),
@@ -1170,7 +1403,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1170
1403
  return React.createElement('section', { key: group.key, className: 'dsm-source' },
1171
1404
  React.createElement('div', { className: 'dsm-source-head' },
1172
1405
  React.createElement('button', { type: 'button', className: 'dsm-source-head-main', 'aria-expanded': open, onClick: () => setCollapsed(Object.assign({}, collapsed, { [group.key]: !open })) },
1173
- React.createElement('span', { className: 'dsm-source-title' }, group.title),
1406
+ React.createElement('span', { className: 'dsm-source-title', title: group.title }, group.title),
1174
1407
  React.createElement('span', { className: 'dsm-count' }, mt('mcp.servers.count', { count: groupRows.length })),
1175
1408
  group.path ? React.createElement('span', { className: 'dsm-path', title: group.path }, group.path) : null)),
1176
1409
  open ? React.createElement('div', { className: 'dsm-source-body' },
@@ -1198,7 +1431,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1198
1431
  React.createElement('div', { className: 'dsm-tools' }, (detail.tools || []).map((tool) => React.createElement('div', { className: 'dsm-tool' + (tool.enabled === false ? ' dsm-tool-off' : ''), key: tool.name },
1199
1432
  React.createElement('div', { className: 'dsm-tool-name-row' },
1200
1433
  React.createElement('div', { className: 'dsm-tool-name' }, tool.name),
1201
- React.createElement(Switch, { on: tool.enabled !== false, disabled: busy !== null, label: mt('mcp.toggleTool') + ' ' + tool.name, onClick: () => toggleTool(detail.row, tool) })),
1434
+ React.createElement(Switch, { on: tool.enabled !== false, disabled: busy !== null || state.locked === true, label: mt('mcp.toggleTool') + ' ' + tool.name, onClick: () => toggleTool(detail.row, tool) })),
1202
1435
  tool.description ? React.createElement('div', { className: 'dsm-tool-desc' }, tool.description) : null,
1203
1436
  (tool.parameters && tool.parameters.length > 0) ? React.createElement('div', { className: 'dsm-tool-params' }, tool.parameters.map((param) => React.createElement('div', { className: 'dsm-tool-param', key: param.key },
1204
1437
  React.createElement('span', { className: 'dsm-tool-param-key' }, param.key + (param.required ? ' *' : '')),
@@ -1231,8 +1464,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1231
1464
  React.createElement(SourceSelect, {
1232
1465
  value: formModal.level,
1233
1466
  options: [
1234
- { value: 'project', label: mt('mcp.field.level.project', { path: state.paths ? state.paths.project : 'profiles/*/cordis.patch.yml' }) },
1235
1467
  { value: 'global', label: mt('mcp.field.level.global', { path: state.paths ? state.paths.global : '~/.dsh/cordis.patch.yml' }) },
1468
+ { value: 'project', label: mt('mcp.field.level.project', { path: state.paths ? state.paths.project : 'profiles/*/cordis.patch.yml' }) },
1236
1469
  ],
1237
1470
  onChange: (value) => setFormValue('level', value),
1238
1471
  })),
@@ -1287,13 +1520,24 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1287
1520
  detailStatus.text + ':' + (detailHint || mt('mcp.detail.registered')))),
1288
1521
  React.createElement('div', { className: 'dsm-detail-section' },
1289
1522
  React.createElement('div', { className: 'dsm-detail-title' }, mt('mcp.detail.note')),
1290
- React.createElement('textarea', { className: 'dsm-control dsm-textarea-sm', value: noteDraft, placeholder: mt('mcp.detail.note.placeholder'), onChange: (ev) => setNoteDraft(ev.target.value) }),
1523
+ React.createElement('textarea', { className: 'dsm-control dsm-textarea-sm', value: noteDraft, maxLength: 200, placeholder: mt('mcp.detail.note.placeholder'), onChange: (ev) => setNoteDraft(ev.target.value) }),
1291
1524
  React.createElement('div', { className: 'dsm-modal-actions' },
1292
1525
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || noteDraft.trim() === String(detail.row.notes || ''), onClick: saveNote }, mt('mcp.detail.note.save')))),
1293
1526
  React.createElement('div', { className: 'dsm-detail-section' },
1294
1527
  React.createElement('div', { className: 'dsm-detail-title' }, mt('mcp.detail.tools', { count: detail.loading ? '…' : (detail.tools || []).length })),
1295
1528
  toolListNode))
1296
1529
 
1530
+ const compactNode = compactConfirm && React.createElement(Modal, {
1531
+ key: 'mcp-compact',
1532
+ title: mt('mcp.compact.title'),
1533
+ closeLabel: mt('btn.close'),
1534
+ onClose: () => setCompactConfirm(false),
1535
+ },
1536
+ React.createElement('p', { className: 'dsm-desc' }, mt('mcp.compact.desc')),
1537
+ React.createElement('div', { className: 'dsm-modal-actions' },
1538
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: () => setCompactConfirm(false) }, mt('btn.close')),
1539
+ React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy !== null, onClick: doCompact }, mt('mcp.compact.confirm'))))
1540
+
1297
1541
  const confirmNode = confirmRow && React.createElement(Modal, {
1298
1542
  key: 'mcp-remove',
1299
1543
  title: mt('mcp.remove.title'),
@@ -1308,18 +1552,19 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1308
1552
  React.createElement('div', { className: 'dsm-head' },
1309
1553
  React.createElement('div', { className: 'dsm-title-block' },
1310
1554
  React.createElement('div', { className: 'dsm-title-row' },
1311
- React.createElement('h2', { className: 'dsm-title' }, 'MCP'),
1312
- VersionBadgeComponent ? React.createElement(VersionBadgeComponent, null) : null),
1555
+ React.createElement('h2', { className: 'dsm-title' }, 'MCP')),
1313
1556
  React.createElement('p', { className: 'dsm-desc' }, mt('mcp.desc'))),
1314
1557
  React.createElement('div', { className: 'dsm-actions' },
1315
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || state.loading, onClick: () => refresh() }, mt('btn.refresh')),
1316
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: openAdd }, mt('mcp.btn.new')),
1317
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null || state.loading, onClick: () => run('mcpm-set-all', { enabled: true }, 'setall') }, mt('mcp.btn.enableAll')),
1318
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null || state.loading, onClick: () => run('mcpm-set-all', { enabled: false }, 'setall') }, mt('mcp.btn.disableAll')))),
1319
- React.createElement('div', { className: 'dsm-summary dsm-summary-3' },
1320
- [[summary.total, mt('mcp.stat.total')], [summary.enabled, mt('mcp.stat.enabled')], [summary.tools, mt('mcp.stat.tools')]].map((item) =>
1321
- React.createElement('div', { key: item[1], className: 'dsm-stat' },
1322
- React.createElement('strong', null, item[0]), item[1]))),
1558
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || state.loading, onClick: () => refresh() }, (busy !== null || state.loading) ? mt('btn.refreshing') : mt('btn.refresh')),
1559
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: state.locked === true, onClick: openAdd }, mt('mcp.btn.new')),
1560
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary dsm-btn-danger', disabled: busy !== null || state.loading, title: mt('mcp.btn.compact.title'), disabled: busy !== null || state.loading || state.locked === true, onClick: () => setCompactConfirm(true) }, mt('mcp.btn.compact')))),
1561
+ state.locked === true ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'status' }, mt('lock.banner')) : null,
1562
+ React.createElement('div', { className: 'dsm-stat-row' },
1563
+ React.createElement('div', { className: 'dsm-summary' },
1564
+ [['total', summary.total, mt('mcp.stat.total')], ['enabled', summary.enabled, mt('mcp.stat.enabled')], ['tools', summary.tools, mt('mcp.stat.tools')]].map((item) =>
1565
+ React.createElement('div', { key: item[0], className: 'dsm-stat' },
1566
+ React.createElement('strong', null, item[1]), item[2]))),
1567
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || state.loading || !summary.total, onClick: () => run('mcpm-set-all', { enabled: summary.enabled < summary.total }, 'setall') }, mt(summary.total > 0 && summary.enabled === summary.total ? 'mcp.btn.disableAll' : 'mcp.btn.enableAll'))),
1323
1568
  React.createElement('div', { className: 'dsm-filters' },
1324
1569
  React.createElement('input', { className: 'dsm-control dsm-search', value: query, 'aria-label': mt('mcp.search'), placeholder: mt('mcp.search.placeholder'), onChange: (ev) => setQuery(ev.target.value) }),
1325
1570
  React.createElement('div', { className: 'dsm-source-filter' },
@@ -1329,9 +1574,22 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1329
1574
  React.createElement(Notice, { kind: 'err', text: state.error }),
1330
1575
  React.createElement(Notice, { kind: 'warn', text: (state.errors && state.errors.length > 0) ? mt('mcp.err.warnings') + state.errors.join(';') : null }),
1331
1576
  React.createElement(Notice, { kind: 'warn', text: (state.warnings && state.warnings.length > 0) ? state.warnings.join(';') : null }),
1577
+ state.revealError ? React.createElement('div', { className: 'dsm-token-panel' },
1578
+ React.createElement(Notice, { kind: 'err', text: state.revealError.text }),
1579
+ // 令牌不对 → 给输入框;宿主根本没配令牌 → 输入框也没用,只给"该怎么配"的说明,
1580
+ // 免得摆一个按了必然失败的按钮。
1581
+ state.revealError.code === 'error.secret.badToken' ? React.createElement('div', { className: 'dsm-token-row' },
1582
+ React.createElement('input', {
1583
+ className: 'dsm-control', type: 'password', value: tokenDraft,
1584
+ placeholder: mt('mcp.token.placeholder'), 'aria-label': mt('mcp.token.label'),
1585
+ onChange: (ev) => setTokenDraft(ev.target.value),
1586
+ }),
1587
+ React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: !tokenDraft.trim(), onClick: applyToken }, mt('mcp.token.save'))) : null,
1588
+ React.createElement('p', { className: 'dsm-help' }, mt('mcp.token.hint'))) : null,
1332
1589
  groupsNode,
1333
1590
  formModalNode,
1334
1591
  detailNode,
1592
+ compactNode,
1335
1593
  confirmNode)
1336
1594
  }
1337
1595
 
@@ -1343,25 +1601,75 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1343
1601
  var em = React.useState(null), editModal = em[0], setEditModal = em[1]
1344
1602
  var cm = React.useState(null), createModal = cm[0], setCreateModal = cm[1]
1345
1603
  var ac = React.useState(null), applyConfirm = ac[0], setApplyConfirm = ac[1]
1346
- var importInput = React.useRef(null)
1347
- function doImport(file) {
1348
- if (!file) return
1349
- var reader = new FileReader()
1350
- reader.onload = function () {
1351
- var id = String(file.name).replace(/\.(md|markdown|txt)$/i, '').trim().replace(/[\\/<>:"|?*]+/g, '-') || 'imported'
1352
- setBusy('import')
1353
- apiCall('agentsmd-import', { id: id, content: String(reader.result) }).then(function (res) {
1354
- setBusy(null)
1355
- if (res && res.ok) refresh()
1356
- else if (res) setState(function (s) { return Object.assign({}, s, { error: res.error }) })
1357
- }).catch(function () { setBusy(null) })
1358
- }
1359
- reader.readAsText(file)
1604
+ // 回收站:null = 关闭;{loading, error, entries}
1605
+ var ts = React.useState(null), trash = ts[0], setTrash = ts[1]
1606
+ var tb = React.useState(false), trashBusy = tb[0], setTrashBusy = tb[1]
1607
+ // D14:导出弹窗(null = 关闭;{busy, result})。
1608
+ var xo = React.useState(null), exportState = xo[0], setExportState = xo[1]
1609
+ /** 打开/刷新回收站(每次操作后重读,界面永远显示磁盘上的真实条目)。 */
1610
+ function loadTrash(keepOpen) {
1611
+ var next = Object.assign({ loading: true, error: null, entries: [] }, keepOpen ? trash || {} : {})
1612
+ setTrash(next)
1613
+ apiCall('agentsmd-trash-list', {}).then(function (res) {
1614
+ if (res && res.ok) setTrash({ loading: false, error: null, entries: res.trash || [] })
1615
+ else setTrash({ loading: false, error: (res && res.error) || t('trash.loadFailed'), entries: [] })
1616
+ }).catch(function (e) { setTrash({ loading: false, error: String((e && e.message) || e), entries: [] }) })
1617
+ }
1618
+ function restorePreset(item) {
1619
+ setTrashBusy(true)
1620
+ apiCall('agentsmd-trash-restore', { id: item.id }).then(function (res) {
1621
+ setTrashBusy(false)
1622
+ if (res && res.ok) { setState(function (s) { return Object.assign({}, s, { notice: t('trash.restored', { name: item.name }) }) }); refresh(); loadTrash(true) }
1623
+ else setTrash(function (cur) { return Object.assign({}, cur, { error: (res && res.error) || t('mcp.msg.failed') }) })
1624
+ }).catch(function (e) { setTrashBusy(false); setTrash(function (cur) { return Object.assign({}, cur, { error: String((e && e.message) || e) }) }) })
1625
+ }
1626
+ function purgePreset(item) {
1627
+ setTrashBusy(true)
1628
+ apiCall('agentsmd-trash-delete', { id: item.id }).then(function (res) {
1629
+ setTrashBusy(false)
1630
+ if (res && res.ok) { setState(function (s) { return Object.assign({}, s, { notice: t('trash.purged', { name: item.name }) }) }); loadTrash(true) }
1631
+ else setTrash(function (cur) { return Object.assign({}, cur, { error: (res && res.error) || t('mcp.msg.failed') }) })
1632
+ }).catch(function (e) { setTrashBusy(false); setTrash(function (cur) { return Object.assign({}, cur, { error: String((e && e.message) || e) }) }) })
1633
+ }
1634
+ // 导入:与技能页同一个 ImportModal(拖拽区 + 文件要求清单 + 结果反馈)。
1635
+ // 一次可选多份 .md,逐个读取正文导入;同名/失败逐条回报,不因一个失败中断其余的。
1636
+ var impOpenState = React.useState(false); var importOpen = impOpenState[0], setImportOpen = impOpenState[1]
1637
+ var impBusyState = React.useState(false); var importBusy = impBusyState[0], setImportBusy = impBusyState[1]
1638
+ var impResultState = React.useState(null); var importResult = impResultState[0], setImportResult = impResultState[1]
1639
+ function importIdOf(name) {
1640
+ return String(name || '').replace(/\.(md|markdown|txt)$/i, '').trim().replace(/[\\/<>:"|?*]+/g, '-') || 'imported'
1641
+ }
1642
+ function doImportFiles(files) {
1643
+ var list = Array.prototype.slice.call(files || [])
1644
+ if (!list.length) return
1645
+ setImportBusy(true); setImportResult(null)
1646
+ var done = [], failed = []
1647
+ var chain = Promise.resolve()
1648
+ list.forEach(function (file) {
1649
+ chain = chain.then(function () {
1650
+ return file.text().then(function (text) {
1651
+ var id = importIdOf(file.name)
1652
+ return apiCall('agentsmd-import', { id: id, content: String(text) }).then(function (res) {
1653
+ if (res && res.ok) done.push(res.id || id)
1654
+ else failed.push(String(file.name) + '(' + ((res && res.error) || '') + ')')
1655
+ })
1656
+ })
1657
+ }).catch(function (e) { failed.push(String(file.name) + '(' + String((e && e.message) || e) + ')') })
1658
+ })
1659
+ chain.then(function () {
1660
+ setImportBusy(false)
1661
+ if (!done.length) setImportResult({ ok: false, text: failed.length ? t('import.skipped', { items: failed.join('、') }) : t('import.none') })
1662
+ else setImportResult({
1663
+ ok: true, warning: failed.length > 0,
1664
+ text: t('agm.result.imported', { count: done.length, names: done.join('、') }) + (failed.length ? t('import.skipped', { items: failed.join('、') }) : ''),
1665
+ })
1666
+ refresh()
1667
+ })
1360
1668
  }
1361
1669
 
1362
1670
  function refresh() {
1363
1671
  apiCall('agentsmd-list', {}).then(function (res) {
1364
- setState(function (s) { return { loading: false, error: res && res.ok ? null : ((res && res.error) || mt('mcp.msg.loadFailed')), presets: (res && res.presets) || [], current: s.current, scenePrompt: (res && res.scenePrompt) || null } })
1672
+ setState(function (s) { return { loading: false, error: res && res.ok ? null : ((res && res.error) || mt('mcp.msg.loadFailed')), presets: (res && res.presets) || [], current: s.current, scenePrompt: (res && res.scenePrompt) || null, anyLocked: (res && res.anyLocked) === true } })
1365
1673
  }).catch(function (e) { setState({ loading: false, error: String((e && e.message) || e), presets: [], current: null, scenePrompt: null }) })
1366
1674
  apiCall('agentsmd-get-current', {}).then(function (res) {
1367
1675
  if (res && res.ok) setState(function (s) { return Object.assign({}, s, { current: res }) })
@@ -1370,11 +1678,6 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1370
1678
  React.useEffect(function () { refresh() }, [])
1371
1679
 
1372
1680
  var cur = state.current
1373
- // 顶部「当前」:场景启用且绑了预设时,**以场景绑定的那份为准**(它就是生效中的基线)。
1374
- var sceneLive = state.scenePrompt && state.scenePrompt.presetId && state.scenePrompt.scene && !state.scenePrompt.missing ? state.scenePrompt : null
1375
- var curLabel = sceneLive
1376
- ? t('agm.current.byScene', { id: sceneLive.presetId, scene: sceneLive.scene })
1377
- : (cur && cur.exists ? (cur.presetId || t('agm.current.external')) : t('agm.current.none'))
1378
1681
  // 改名结果提示:短暂显示后自动消失(与场景页的结果条同一节奏)。
1379
1682
  React.useEffect(function () {
1380
1683
  if (!state.notice) return undefined
@@ -1385,7 +1688,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1385
1688
  function openEdit(p) {
1386
1689
  // id 可改:改名 = 目录改名,宿主会把场景绑定一起改名(见 rules-rebind-prompt)。
1387
1690
  apiCall('agentsmd-read', { id: p.id }).then(function (res) {
1388
- if (res && res.ok) setEditModal({ id: p.id, nextId: p.id, content: res.content })
1691
+ if (res && res.ok) setEditModal({ id: p.id, nextId: p.id, content: res.content, description: p.description || '' })
1389
1692
  }).catch(function () {})
1390
1693
  }
1391
1694
  function doApply(id) {
@@ -1402,13 +1705,15 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1402
1705
  else refresh()
1403
1706
  }).catch(function (e) { setBusy(null); setApplyConfirm(null); setState(function (s) { return Object.assign({}, s, { error: String((e && e.message) || e) }) }) })
1404
1707
  }
1405
- function doCreate(id, from, content) {
1708
+ function doCreate(id, from, content, description) {
1406
1709
  setBusy('create')
1407
1710
  // content 优先(用户直接写的内容);留空才走 from 复制;都没有 = 宿主空模板。
1408
1711
  apiCall('agentsmd-create', {
1409
1712
  id: id,
1410
1713
  ...(content ? { content: content } : {}),
1411
1714
  ...(from ? { from: from } : {}),
1715
+ // 描述只给使用者看(存 meta.json,不进 AGENTS.md)。
1716
+ description: String(description == null ? '' : description),
1412
1717
  }).then(function (res) {
1413
1718
  setBusy(null)
1414
1719
  if (res && res.ok) { setCreateModal(null); refresh() }
@@ -1417,7 +1722,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1417
1722
  }
1418
1723
  function doUpdate() {
1419
1724
  setBusy('update')
1420
- apiCall('agentsmd-update', { id: editModal.id, nextId: String(editModal.nextId || editModal.id), content: editModal.content }).then(function (res) {
1725
+ apiCall('agentsmd-update', { id: editModal.id, nextId: String(editModal.nextId || editModal.id), content: editModal.content, description: String(editModal.description == null ? '' : editModal.description) }).then(function (res) {
1421
1726
  setBusy(null)
1422
1727
  if (res && res.ok) {
1423
1728
  setEditModal(null)
@@ -1449,7 +1754,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1449
1754
  return React.createElement('div', { key: p.id, className: 'dsm-source' },
1450
1755
  React.createElement('div', { className: 'dsm-source-head' },
1451
1756
  React.createElement('div', { className: 'dsm-source-head-main' },
1452
- React.createElement('span', { className: 'dsm-source-title' }, p.id),
1757
+ React.createElement('span', { className: 'dsm-source-title', title: p.id }, p.id),
1453
1758
  p.active ? React.createElement('span', { className: 'dsm-tag dsm-tag-on', title: viaScene ? t('agm.active.hint.scene', { scene: (state.scenePrompt && state.scenePrompt.scene) || '' }) : t('agm.active.hint.file') }, t('agm.active')) : null,
1454
1759
  !p.active && p.fileApplied ? React.createElement('span', { className: 'dsm-tag', title: t('agm.file.applied.hint') }, t('agm.file.applied')) : null,
1455
1760
  boundByScene && !viaScene ? React.createElement('span', { className: 'dsm-tag' + (boundByScene.missing ? ' dsm-tag-off' : ''), title: t('agm.scene.tag.hint') },
@@ -1457,31 +1762,67 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1457
1762
  ? t('agm.scene.missing', { scene: boundByScene.scene })
1458
1763
  : t('agm.scene.tag', { scene: boundByScene.scene })) : null),
1459
1764
  React.createElement('div', { className: 'dsm-source-actions' },
1460
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', onClick: function () { openEdit(p) } }, t('agm.btn.edit')),
1461
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null, onClick: function () { setApplyConfirm({ id: p.id }) }, title: t('agm.apply.hint') }, p.fileApplied ? t('agm.btn.reapply') : t('agm.btn.apply')),
1462
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy !== null || p.active === true, title: p.active === true ? t('agm.btn.delete.blocked') : '', onClick: function () { setApplyConfirm({ id: p.id, remove: true }) } }, t('agm.btn.delete')))))
1765
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: state.anyLocked === true, onClick: function () { openEdit(p) } }, t('agm.btn.edit')),
1766
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null || state.anyLocked === true, onClick: function () { setApplyConfirm({ id: p.id }) }, title: t('agm.apply.hint') }, p.fileApplied ? t('agm.btn.reapply') : t('agm.btn.apply')),
1767
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy !== null || p.active === true || state.anyLocked === true, title: p.active === true ? t('agm.btn.delete.blocked') : t('scenes.lock.blockedEdit'), onClick: function () { setApplyConfirm({ id: p.id, remove: true }) } }, t('agm.btn.delete')))), p.description ? React.createElement("div", { key: "desc", className: "dsm-source-note", title: p.description }, p.description) : null)
1463
1768
  }))
1464
1769
 
1465
1770
  return React.createElement('section', { className: 'dsm-section' },
1466
1771
  React.createElement('div', { className: 'dsm-head' },
1467
1772
  React.createElement('div', { className: 'dsm-title-block' },
1468
1773
  React.createElement('div', { className: 'dsm-title-row' },
1469
- React.createElement('h2', { className: 'dsm-title' }, t('tabs.prompts')),
1470
- VersionBadgeComponent ? React.createElement(VersionBadgeComponent, null) : null),
1471
- React.createElement('p', { className: 'dsm-desc' }, t('prompts.desc')),
1472
- // 当前基线:场景启用并绑了预设时,这里显示的就是**场景绑的那份**(用户裁定)。
1473
- React.createElement('p', { className: 'dsm-help' }, t('agm.current.label', { label: curLabel }))),
1774
+ React.createElement('h2', { className: 'dsm-title' }, t('tabs.prompts'))),
1775
+ React.createElement('p', { className: 'dsm-desc' }, t('prompts.desc'))),
1474
1776
  React.createElement('div', { className: 'dsm-actions' },
1475
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: function () { setCreateModal({ id: '', from: '', content: '', error: null }) } }, t('agm.btn.new')),
1476
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null, onClick: function () { if (importInput.current) importInput.current.click() } }, t('agm.btn.import')))),
1777
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: state.loading, onClick: function () { refresh() } }, state.loading ? t('btn.refreshing') : t('btn.refresh')),
1778
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: state.anyLocked === true, onClick: function () { setCreateModal({ id: '', from: '', content: '', description: '', error: null }) } }, t('agm.btn.new')),
1779
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || state.anyLocked === true, onClick: function () { setImportResult(null); setImportOpen(true) } }, t('agm.btn.import')),
1780
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || !state.presets.length, onClick: function () { setExportState({ busy: false, result: null }) } }, t('export.presets')),
1781
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: function () { loadTrash(false) } }, t('trash.btn.open')))),
1782
+ state.anyLocked === true ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'status' }, t('lock.banner')) : null,
1783
+ React.createElement('div', { className: 'dsm-summary', style: { '--dsm-stat-cols': '2' } },
1784
+ [['total', state.presets.length, t('agm.stat.total')], ['active', state.presets.filter(function (p) { return p.active }).length, t('agm.stat.active')]].map(function (item) {
1785
+ return React.createElement('div', { key: item[0], className: 'dsm-stat' },
1786
+ React.createElement('strong', null, item[1]), item[2])
1787
+ })),
1477
1788
  body,
1478
1789
  state.notice ? React.createElement(Notice, { kind: 'ok', text: state.notice }) : null,
1790
+ exportState ? React.createElement(ExportModal, {
1791
+ key: 'export', t: t, title: t('export.presets'),
1792
+ items: state.presets.map(function (p) { return { key: p.id, name: p.id, desc: p.description || '' } }),
1793
+ busy: exportState.busy, result: exportState.result,
1794
+ onClose: function () { setExportState(null) },
1795
+ onSubmit: function (names, outDir) {
1796
+ setExportState({ busy: true, result: null });
1797
+ apiCall('bundle-export', { kind: 'presets', names: names, outDir: outDir }).then(function (res) {
1798
+ if (res && res.ok) setExportState({ busy: false, result: { ok: true, text: exportResultText(t, res) } });
1799
+ else setExportState({ busy: false, result: { ok: false, text: translateError(t, res) } });
1800
+ }).catch(function (e) { setExportState({ busy: false, result: { ok: false, text: String((e && e.message) || e) } }) });
1801
+ },
1802
+ }) : null,
1803
+ trash ? React.createElement(TrashModal, {
1804
+ t: t,
1805
+ title: t('trash.title'),
1806
+ groupTitle: t('trash.group.presets'),
1807
+ groupSub: t('trash.section.presets.sub'),
1808
+ locked: state.anyLocked === true,
1809
+ entries: trash.entries,
1810
+ loading: trash.loading,
1811
+ error: trash.error,
1812
+ busy: trashBusy,
1813
+ onClose: function () { setTrash(null) },
1814
+ onRestore: restorePreset,
1815
+ onPurge: purgePreset,
1816
+ }) : null,
1479
1817
  // 编辑:id 与内容都可改(改 id = 目录改名,场景绑定由宿主一起改名)。
1480
- editModal ? React.createElement(Modal, { title: t('agm.edit.title') + ' · ' + editModal.id, closeLabel: t('btn.cancel'), onClose: function () { setEditModal(null) } },
1818
+ editModal ? React.createElement(Modal, { title: t('agm.edit.title') + ' · ' + editModal.id, closeLabel: t('btn.close'), onClose: function () { setEditModal(null) } },
1481
1819
  React.createElement('div', { className: 'dsm-field' },
1482
1820
  React.createElement('label', { className: 'dsm-label' }, t('agm.field.id')),
1483
1821
  React.createElement('input', { className: 'dsm-control', value: editModal.nextId || '', onChange: function (e) { setEditModal(Object.assign({}, editModal, { nextId: e.target.value, error: null })) } }),
1484
1822
  React.createElement('p', { className: 'dsm-help' }, t('agm.field.id.hint'))),
1823
+ React.createElement('div', { className: 'dsm-field' },
1824
+ React.createElement('label', { className: 'dsm-label' }, t('agm.field.desc')),
1825
+ React.createElement('input', { className: 'dsm-control', placeholder: t('agm.field.desc.placeholder'), value: editModal.description || '', onChange: function (e) { setEditModal(Object.assign({}, editModal, { description: e.target.value })) } })),
1485
1826
  React.createElement('div', { className: 'dsm-field' },
1486
1827
  React.createElement('label', { className: 'dsm-label' }, t('agm.field.content')),
1487
1828
  React.createElement('textarea', { className: 'dsm-control dsm-textarea-lg', value: editModal.content, onChange: function (e) { setEditModal(Object.assign({}, editModal, { content: e.target.value })) } })),
@@ -1490,11 +1831,14 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1490
1831
  React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy !== null || !String(editModal.nextId || '').trim(), onClick: doUpdate }, t('agm.btn.save')))) : null,
1491
1832
  // 新建:id + **正文文本框**(用户直接写内容);「从现有预设复制」是可选下拉,
1492
1833
  // 选中后把那份正文填进文本框,仍可继续改——不再是容易被当成搜索框的文本输入。
1493
- createModal ? React.createElement(Modal, { title: t('agm.btn.new'), closeLabel: t('btn.cancel'), onClose: function () { setCreateModal(null) } },
1834
+ createModal ? React.createElement(Modal, { title: t('agm.btn.new'), closeLabel: t('btn.close'), onClose: function () { setCreateModal(null) } },
1494
1835
  React.createElement('div', { className: 'dsm-field' },
1495
1836
  React.createElement('label', { className: 'dsm-label' }, t('agm.field.id')),
1496
1837
  React.createElement('input', { className: 'dsm-control', placeholder: t('agm.field.id.placeholder'), value: createModal.id, onChange: function (e) { setCreateModal(Object.assign({}, createModal, { id: e.target.value, error: null })) } }),
1497
1838
  React.createElement('p', { className: 'dsm-help' }, t('agm.field.id.hint'))),
1839
+ React.createElement('div', { className: 'dsm-field' },
1840
+ React.createElement('label', { className: 'dsm-label' }, t('agm.field.desc')),
1841
+ React.createElement('input', { className: 'dsm-control', placeholder: t('agm.field.desc.placeholder'), value: createModal.description || '', onChange: function (e) { setCreateModal(Object.assign({}, createModal, { description: e.target.value })) } })),
1498
1842
  (state.presets || []).length ? React.createElement('div', { className: 'dsm-field' },
1499
1843
  React.createElement('label', { className: 'dsm-label' }, t('agm.field.copyFrom')),
1500
1844
  React.createElement(SourceSelect, {
@@ -1507,8 +1851,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1507
1851
  React.createElement('textarea', { className: 'dsm-control dsm-textarea-lg', placeholder: t('agm.field.content.placeholder'), value: createModal.content || '', onChange: function (e) { setCreateModal(Object.assign({}, createModal, { content: e.target.value })) } })),
1508
1852
  createModal.error ? React.createElement('div', { className: 'dsm-feedback dsm-error' }, createModal.error) : null,
1509
1853
  React.createElement('div', { className: 'dsm-modal-actions' },
1510
- React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy !== null || !String(createModal.id || '').trim(), onClick: function () { doCreate(createModal.id, createModal.from, createModal.content) } }, t('agm.btn.create')))) : null,
1511
- applyConfirm ? React.createElement(Modal, { title: applyConfirm.remove ? (t('agm.remove.title') + ' · ' + applyConfirm.id) : (t('agm.applyModal.title') + ' · ' + applyConfirm.id), closeLabel: t('btn.cancel'), onClose: function () { setApplyConfirm(null) } },
1854
+ React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy !== null || !String(createModal.id || '').trim(), onClick: function () { doCreate(createModal.id, createModal.from, createModal.content, createModal.description) } }, t('agm.btn.create')))) : null,
1855
+ 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) } },
1512
1856
  applyConfirm.remove
1513
1857
  ? React.createElement('p', { className: 'dsm-help' }, t('agm.remove.title') + ' ' + applyConfirm.id + t('agm.remove.suffix'))
1514
1858
  : React.createElement('div', null,
@@ -1516,7 +1860,13 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1516
1860
  React.createElement('div', { className: 'dsm-feedback dsm-warning' }, t('agm.apply.note'))),
1517
1861
  React.createElement('div', { className: 'dsm-modal-actions' },
1518
1862
  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,
1519
- React.createElement('input', { ref: importInput, type: 'file', accept: '.md,.markdown,.txt', className: 'dsm-hidden-input', onChange: function (e) { if (e.target.files && e.target.files[0]) doImport(e.target.files[0]); e.target.value = ''; } }))
1863
+ importOpen ? React.createElement(ImportModal, {
1864
+ key: 'imp', t: t, title: t('agm.btn.import'), accept: '.md',
1865
+ busy: importBusy, result: importResult,
1866
+ requirements: [t('upload.requirement.prompt.1'), t('upload.requirement.prompt.2'), t('upload.requirement.prompt.3')],
1867
+ onClose: function () { setImportOpen(false); setImportResult(null) },
1868
+ onSubmit: function (entries, files) { doImportFiles(files) },
1869
+ }) : null)
1520
1870
  }
1521
1871
 
1522
1872
  // 「工具」设置页:一个侧栏项,内部 tab 切换 场景 / MCP / 技能 / 子智能体 / 提示词 / 记忆 / 会话。
@@ -1527,8 +1877,21 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1527
1877
  React.useEffect(function () {
1528
1878
  // 设置宿主的真正滚动容器,而不是 body:不同页签内容高度不同,
1529
1879
  // 让滚动条按需出现会改变 options 宽度,页签/GitHub 随之左右跳动。
1880
+ //
1881
+ // ⚠️ 必须**向上查找**而不是固定跳两级:`.dsm-tabs-shell` 的 position:sticky
1882
+ // 依赖祖先链上没有被 overflow 裁剪的层,宿主 DOM 结构一变,「上两级」就可能
1883
+ // 指到非滚动容器(sticky 随之失效,页签被内容滚走)。
1530
1884
  var section = sectionRef.current
1531
- var host = section && section.parentElement && section.parentElement.parentElement
1885
+ if (!section) return undefined
1886
+ var host = null
1887
+ var node = section.parentElement
1888
+ while (node && node !== document.body) {
1889
+ var oy = window.getComputedStyle(node).overflowY
1890
+ if (oy === 'auto' || oy === 'scroll') { host = node; break }
1891
+ node = node.parentElement
1892
+ }
1893
+ // 找不到(宿主换了实现)→ 退回原来的「上两级」,保持兼容。
1894
+ if (!host) host = section.parentElement && section.parentElement.parentElement
1532
1895
  if (!host) return undefined
1533
1896
  host.classList.add('dsm-settings-scroll-host')
1534
1897
  return function () { host.classList.remove('dsm-settings-scroll-host') }
@@ -1544,14 +1907,14 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1544
1907
  return React.createElement('section', { ref: sectionRef, className: 'dsm-section' },
1545
1908
  React.createElement('div', { className: 'dsm-tabs-shell' },
1546
1909
  React.createElement('div', { className: 'dsm-tabs' },
1547
- React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'scenes' ? ' dsm-tab-active' : ''), onClick: function () { setActive('scenes') } }, t('tabs.scenes')),
1548
- React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'mcp' ? ' dsm-tab-active' : ''), onClick: function () { setActive('mcp') } }, 'MCP'),
1549
- React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'skills' ? ' dsm-tab-active' : ''), onClick: function () { setActive('skills') } }, t('tabs.skills')),
1550
- React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'subagents' ? ' dsm-tab-active' : ''), onClick: function () { setActive('subagents') } }, t('tabs.subagents')),
1551
- React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'prompts' ? ' dsm-tab-active' : ''), onClick: function () { setActive('prompts') } }, t('tabs.prompts')),
1552
- React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'memory' ? ' dsm-tab-active' : ''), onClick: function () { setActive('memory') } }, t('tabs.memory')),
1553
- React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'sessions' ? ' dsm-tab-active' : ''), onClick: function () { setActive('sessions') } }, t('tabs.sessions')),
1554
- React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'compat' ? ' dsm-tab-active' : ''), onClick: function () { setActive('compat') } }, t('tabs.compat'))),
1910
+ React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'scenes' ? ' dsm-tab-active' : ''), onClick: function () { setActive('scenes') }, title: t('tabs.scenes') }, t('tabs.scenes')),
1911
+ React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'mcp' ? ' dsm-tab-active' : ''), onClick: function () { setActive('mcp') }, title: 'MCP' }, 'MCP'),
1912
+ React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'skills' ? ' dsm-tab-active' : ''), onClick: function () { setActive('skills') }, title: t('tabs.skills') }, t('tabs.skills')),
1913
+ React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'subagents' ? ' dsm-tab-active' : ''), onClick: function () { setActive('subagents') }, title: t('tabs.subagents') }, t('tabs.subagents')),
1914
+ React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'prompts' ? ' dsm-tab-active' : ''), onClick: function () { setActive('prompts') }, title: t('tabs.prompts') }, t('tabs.prompts')),
1915
+ React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'memory' ? ' dsm-tab-active' : ''), onClick: function () { setActive('memory') }, title: t('tabs.memory') }, t('tabs.memory')),
1916
+ React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'sessions' ? ' dsm-tab-active' : ''), onClick: function () { setActive('sessions') }, title: t('tabs.sessions') }, t('tabs.sessions')),
1917
+ React.createElement('button', { type: 'button', className: 'dsm-tab' + (active === 'compat' ? ' dsm-tab-active' : ''), onClick: function () { setActive('compat') }, title: t('tabs.compat') }, t('tabs.compat'))),
1555
1918
  React.createElement(FeedbackLinks, null)),
1556
1919
  page)
1557
1920
  }
@@ -1606,6 +1969,9 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1606
1969
  var OP_BY_PATH = { "/state": "skill-state", "/enable": "skill-enable", "/disable": "skill-disable",
1607
1970
  "/prefer": "skill-prefer", "/unprefer": "skill-unprefer",
1608
1971
  "/source-enable": "skill-source-enable", "/source-disable": "skill-source-disable",
1972
+ // 「移除来源 / 恢复读取」两个按钮曾经打的是这两个路径,但映射表里没有它们 ——
1973
+ // callApi 直接 reject("unknown action"),弹窗又 catch 掉了,按钮点下去毫无反应。
1974
+ "/source-remove": "skill-source-remove", "/source-restore": "skill-source-restore",
1609
1975
  "/delete": "skill-delete", "/trash-restore": "skill-trash-restore", "/trash-delete": "skill-trash-delete",
1610
1976
  "/detail": "skill-detail", "/create": "skill-create", "/import": "skill-import",
1611
1977
  "/upload": "skill-upload", "/browse": "skill-browse" };
@@ -1697,7 +2063,12 @@ function callApi(path, options) {
1697
2063
  }
1698
2064
  function Switch(props) { return h("button", { type: "button", className: "dsm-switch" + (props.on ? " dsm-switch-on" : ""), role: "switch", "aria-checked": props.on, "aria-label": props.label, disabled: props.disabled, onClick: props.onClick }); }
1699
2065
  function GithubMark16() { return h("svg", { viewBox: "0 0 16 16", width: 16, height: 16, "aria-hidden": true, focusable: "false" }, h("path", { fill: "currentColor", d: "M8 0a8 8 0 0 0-2.53 15.59c.4.074.547-.173.547-.385 0-.19-.007-.693-.01-1.36-2.226.484-2.695-1.073-2.695-1.073-.364-.924-.89-1.17-.89-1.17-.726-.496.055-.486.055-.486.803.056 1.225.824 1.225.824.714 1.223 1.872.87 2.328.665.072-.517.28-.87.508-1.07-1.777-.202-3.645-.888-3.645-3.956 0-.874.31-1.588.823-2.148-.083-.202-.357-1.017.078-2.12 0 0 .672-.215 2.2.82A7.65 7.65 0 0 1 8 4.8c.68.003 1.365.092 2.004.27 1.527-1.035 2.197-.82 2.197-.82.437 1.103.162 1.918.08 2.12.513.56.822 1.274.822 2.148 0 3.076-1.872 3.752-3.654 3.95.288.248.544.735.544 1.482 0 1.07-.01 1.932-.01 2.195 0 .214.144.463.55.384A8.001 8.001 0 0 0 8 0Z" })); }
1700
- function Modal(props) { var ref = react.useRef(null); react.useEffect(function () { if (ref.current) ref.current.focus(); }, []); return h("div", { className: "dsm-mask", onMouseDown: function (e) { if (e.target === e.currentTarget) props.onClose(); } }, h("div", { ref: ref, tabIndex: -1, className: "dsm-modal" + (props.wide ? " dsm-modal-wide" : "") + (props.className ? " " + props.className : ""), role: "dialog", "aria-modal": "true", onKeyDown: function (e) { if (!handleModalEscape(e, props.onClose)) trapModalFocus(e.currentTarget, e); } }, h("div", { className: "dsm-modal-head" }, h("h3", { className: "dsm-modal-title" }, props.title), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", onClick: props.onClose }, props.closeLabel)), props.children)); }
2066
+ /**
2067
+ * 弹窗原语。`list: true` 时走「列表类」三段式:固定头 / 滚动体 / 固定底,
2068
+ * 配 `.dsm-modal-list`(宽高都固定),滚动只发生在 body 这一层 —— 条目数变化时
2069
+ * 弹窗不跳、也不会出现嵌套滚动条。普通表单/确认类**不传 list**,结构保持原样。
2070
+ */
2071
+ function Modal(props) { var ref = react.useRef(null); react.useEffect(function () { if (ref.current) ref.current.focus(); }, []); var body = props.list ? h("div", { className: "dsm-modal-body" }, props.children) : props.children; return h("div", { className: "dsm-mask", onMouseDown: function (e) { if (e.target === e.currentTarget) props.onClose(); } }, h("div", { ref: ref, tabIndex: -1, className: "dsm-modal" + (props.wide ? " dsm-modal-wide" : "") + (props.className ? " " + props.className : ""), role: "dialog", "aria-modal": "true", onKeyDown: function (e) { if (!handleModalEscape(e, props.onClose)) trapModalFocus(e.currentTarget, e); } }, h("div", { className: "dsm-modal-head" }, h("h3", { className: "dsm-modal-title" }, props.title), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", onClick: props.onClose }, props.closeLabel)), body)); }
1701
2072
  /**
1702
2073
  * 全插件唯一的「操作结果」呈现方式:成功 = 右下角浮层(固定定位,不改变页面高度),
1703
2074
  * 警告 / 错误 = 文档流内横幅(需要用户处理,常驻到下一次操作)。所有页面共用,
@@ -1722,6 +2093,63 @@ function callApi(path, options) {
1722
2093
  props.body,
1723
2094
  props.foot || null)
1724
2095
  }
2096
+ /**
2097
+ * 通用回收站弹窗:一行一个条目(名字 + 删除时间),右侧「恢复 / 永久删除」。
2098
+ *
2099
+ * 子智能体 / 场景 / 提示词预设三处共用;永久删除就在本弹窗里就地二次确认,
2100
+ * 不再叠一层弹窗。恢复失败的原因显示在弹窗顶部 —— 不静默(同名已存在时必须说清)。
2101
+ */
2102
+ function TrashModal(props) {
2103
+ const t = props.t
2104
+ const c = React.useState(null)
2105
+ const confirmId = c[0], setConfirmId = c[1]
2106
+ const entries = props.entries || []
2107
+ const row = function (item) {
2108
+ const confirming = confirmId === item.id
2109
+ const actions = confirming
2110
+ ? [
2111
+ React.createElement('span', { className: 'dsm-note', key: 'ask' }, t('trash.purge.confirm')),
2112
+ React.createElement('button', { key: 'yes', type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: props.busy, onClick: function () { setConfirmId(null); props.onPurge(item) } }, t('btn.delete.forever')),
2113
+ React.createElement('button', { key: 'no', type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: props.busy, onClick: function () { setConfirmId(null) } }, t('btn.cancel')),
2114
+ ]
2115
+ : [
2116
+ 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')),
2117
+ 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')),
2118
+ ]
2119
+ return React.createElement('div', { className: 'dsm-trash-item', key: item.id },
2120
+ React.createElement('div', { className: 'dsm-trash-main' },
2121
+ React.createElement('div', { className: 'dsm-name' }, item.name),
2122
+ React.createElement('div', { className: 'dsm-note' }, t('trash.deletedAt', {
2123
+ time: item.deletedAt ? new Date(item.deletedAt).toLocaleString() : '—',
2124
+ }))),
2125
+ actions)
2126
+ }
2127
+ const body = props.loading
2128
+ ? React.createElement('div', { className: 'dsm-empty' }, t('memory.loading'))
2129
+ : entries.length === 0
2130
+ ? React.createElement('div', { className: 'dsm-empty' }, t('trash.empty'))
2131
+ : entries.map(row)
2132
+ // 列表类弹窗:宽高都固定(`.dsm-modal-list`),滚动只发生在 Modal 的 body 这一层 ——
2133
+ // 条目数变化时弹窗不跳,也不会出现「弹窗滚 + 组内滚」的嵌套滚动条。
2134
+ return React.createElement(Modal, {
2135
+ key: 'trash-modal',
2136
+ title: props.title,
2137
+ closeLabel: t('btn.close'),
2138
+ list: true,
2139
+ className: 'dsm-modal-list',
2140
+ onClose: props.onClose,
2141
+ },
2142
+ props.error ? React.createElement(Notice, { kind: 'err', text: props.error }) : null,
2143
+ React.createElement('div', { className: 'dsm-trash-group' },
2144
+ React.createElement('div', { className: 'dsm-trash-group-head' },
2145
+ React.createElement('div', { className: 'dsm-trash-group-head-row' },
2146
+ React.createElement('span', { className: 'dsm-trash-group-title' }, props.groupTitle),
2147
+ React.createElement('span', { className: 'dsm-count' }, t('trash.items.count', { count: entries.length }))),
2148
+ // 后果说明进组头(sticky 跟着滚):这组删的是记录还是文件,一眼可辨。
2149
+ props.groupSub ? React.createElement('p', { className: 'dsm-trash-group-sub' }, props.groupSub) : null),
2150
+ React.createElement('div', { className: 'dsm-trash-group-body' }, body)))
2151
+ }
2152
+
1725
2153
  /** 勾选行:复选框 + 名称/说明 + 右侧指标 + 可选行内动作。 */
1726
2154
  function pickRow(props) {
1727
2155
  return h('label', { key: props.key, className: 'dsm-pick' },
@@ -1745,13 +2173,17 @@ function callApi(path, options) {
1745
2173
  h('span', { className: 'dsm-seg-group-line' }))
1746
2174
  }
1747
2175
  /** 段头公共动作:全选 / 清空 / 移除段(未定义时改为「添加」)。 */
2176
+ /**
2177
+ * 段头动作。**批量动作合二为一**(D11):同一时刻只有一个方向是合理的 ——
2178
+ * 还有没勾的 → 「全选」;全勾了 → 「全不选」。并列两个必然有一个是废话。
2179
+ * 未定义的可选段只给「添加」;「移除段」不属于全选族,保留。
2180
+ */
1748
2181
  function segActions(props) {
1749
2182
  if (!props.defined) return [h('button', { key: 'add', type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: props.busy, onClick: props.onAdd }, props.addLabel)]
1750
- return [
1751
- h('button', { key: 'all', type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: props.busy, onClick: props.onAll }, props.selectAllLabel),
1752
- h('button', { key: 'clear', type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: props.busy, onClick: props.onClear }, props.clearLabel),
1753
- h('button', { key: 'rm', type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: props.busy, onClick: props.onRemove }, props.removeLabel),
1754
- ]
2183
+ var out = [h('button', { key: 'all', type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: props.busy, onClick: props.allChecked ? props.onClear : props.onAll }, props.allChecked ? props.clearLabel : props.selectAllLabel)]
2184
+ // 常驻段(记忆,P5 单一真相源)没有「段定义」可移除 → 不传 onRemove 即不渲染。
2185
+ if (props.onRemove) out.push(h('button', { key: 'rm', type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: props.busy, onClick: props.onRemove }, props.removeLabel))
2186
+ return out
1755
2187
  }
1756
2188
  /** 段脚注:段已定义但一项未勾 = 该域全部停用(必须显式提示,否则像没保存上)。 */
1757
2189
  function segFoot(defined, count, emptyLabel) {
@@ -1767,17 +2199,28 @@ function callApi(path, options) {
1767
2199
  // 通用导入弹窗(人设 / 记忆共用):拖放或点选 .md / .zip → 已选计数 → 交由调用方走自己的写 op。
1768
2200
  // t 由调用方注入(各页的 t 是局部函数,不在模块作用域);extra 用于注入「导入到场景」一类的额外字段。
1769
2201
  function ImportModal(props) {
1770
- var st = react.useState([]); var files = st[0], setFiles = st[1]
2202
+ // 同时持有原始 File[] 与 base64 entries:前者给需要 `_dssmPath`(拖拽保留相对路径)
2203
+ // 或自己做校验的调用方(技能页),后者给只关心 `{name, data}` 的调用方。
2204
+ var st = react.useState([]); var picked = st[0], setPicked = st[1]
2205
+ var es = react.useState([]); var entries = es[0], setEntries = es[1]
1771
2206
  var ref = react.useRef(null)
2207
+ var folderRef = react.useRef(null)
1772
2208
  var t = props.t
1773
2209
  function add(list) {
1774
- var picked = Array.prototype.slice.call(list || [])
1775
- if (!picked.length) return
1776
- Promise.all(picked.map(function (f) { return f.arrayBuffer().then(function (buf) { return { name: uploadFilePath(f), data: bytesToBase64(buf) } }) }))
1777
- .then(function (entries) { setFiles(function (prev) { return prev.concat(entries) }) })
2210
+ var next = Array.prototype.slice.call(list || [])
2211
+ if (!next.length) return
2212
+ Promise.all(next.map(function (f) { return f.arrayBuffer().then(function (buf) { return { name: uploadFilePath(f), data: bytesToBase64(buf) } }) }))
2213
+ .then(function (added) {
2214
+ setPicked(function (prev) { return prev.concat(next) })
2215
+ setEntries(function (prev) { return prev.concat(added) })
2216
+ })
1778
2217
  .catch(function () {})
1779
2218
  }
1780
- return h(Modal, { className: "dsm-modal-import", title: props.title, closeLabel: t("btn.cancel"), onClose: props.onClose },
2219
+ function dropAt(index) {
2220
+ setPicked(function (prev) { return prev.filter(function (_, j) { return j !== index }) })
2221
+ setEntries(function (prev) { return prev.filter(function (_, j) { return j !== index }) })
2222
+ }
2223
+ return h(Modal, { className: "dsm-modal-sm", title: props.title, closeLabel: t("btn.close"), onClose: props.onClose },
1781
2224
  h("div", { className: "dsm-form" },
1782
2225
  props.extra || null,
1783
2226
  h("div", {
@@ -1787,13 +2230,97 @@ function callApi(path, options) {
1787
2230
  onDragLeave: function (e) { if (e.currentTarget.classList) e.currentTarget.classList.remove("dsm-dropzone-active") },
1788
2231
  onDrop: function (e) { e.preventDefault(); if (e.currentTarget.classList) e.currentTarget.classList.remove("dsm-dropzone-active"); droppedFiles(e.dataTransfer).then(add) },
1789
2232
  },
1790
- h("div", { className: "dsm-dropzone-title" }, t("import.pick")),
1791
- h("div", { className: "dsm-dropzone-copy" }, props.hint),
1792
- files.length ? h("div", { className: "dsm-help" }, t("import.selected", { count: files.length })) : null),
1793
- h("input", { ref: ref, type: "file", multiple: true, accept: ".md,.zip", className: "dsm-hidden-input", onChange: function (e) { add(e.target.files); e.target.value = "" } })),
2233
+ h("span", { className: "dsm-dropzone-title" }, t("upload.drop.title")),
2234
+ h("span", { className: "dsm-dropzone-copy" }, props.hint || t("upload.drop.copy"))),
2235
+ h("input", { ref: ref, type: "file", multiple: true, accept: props.accept || ".md,.zip", className: "dsm-hidden-input", onChange: function (e) { add(e.target.files); e.target.value = "" } }),
2236
+ // 「选择文件夹」:`webkitdirectory` 的隐藏 input 由组件自己渲染,
2237
+ // 调用方不必各自复制一份(技能页原来就有一份)。
2238
+ props.allowFolder ? h("input", { ref: folderRef, type: "file", multiple: true, webkitdirectory: "", directory: "", className: "dsm-hidden-input", onChange: function (e) { add(e.target.files); e.target.value = "" } }) : null,
2239
+ // 已选文件:一行一个(名字 + 大小 + 移除),与技能页同一形态。
2240
+ picked.length ? h("div", null, picked.map(function (f, i) {
2241
+ return h("div", { key: i, className: "dsm-file", title: f.name },
2242
+ h("span", { className: "dsm-file-kind", "aria-hidden": "true" }, "FILE"),
2243
+ h("span", { className: "dsm-file-name" }, f.name),
2244
+ h("span", { className: "dsm-file-meta" }, fmtBytes(f.size)),
2245
+ h("button", { type: "button", className: "dsm-file-remove", disabled: props.busy, "aria-label": t("upload.remove") + " " + f.name, onClick: function () { dropAt(i) } }, "×"))
2246
+ })) : null,
2247
+ props.result ? h("div", { className: "dsm-feedback" + (props.result.warning ? " dsm-warning" : props.result.ok ? "" : " dsm-error"), role: "alert" }, props.result.text) : null,
2248
+ // 文件要求清单(可选):调用方按页传,结构上限制「这页需要什么格式」的表达。
2249
+ props.requirements && props.requirements.length ? h("div", { className: "dsm-upload-requirements" },
2250
+ h("div", { className: "dsm-label" }, t("upload.requirements")),
2251
+ h("ul", null, props.requirements.map(function (r, i) { return h("li", { key: i }, r) }))) : null),
1794
2252
  h("div", { className: "dsm-modal-actions" },
1795
- files.length ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", disabled: props.busy, onClick: function () { setFiles([]) } }, t("import.clear")) : null,
1796
- h("button", { type: "button", className: "dsm-btn dsm-btn-primary", disabled: props.busy || !files.length, onClick: function () { props.onSubmit(files) } }, props.busy ? t("upload.importing") : t("import.submit"))))
2253
+ picked.length ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", disabled: props.busy, onClick: function () { setPicked([]); setEntries([]) } }, t("import.clear")) : null,
2254
+ props.allowFolder ? h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: props.busy, onClick: function () { if (folderRef.current) folderRef.current.click() } }, t("btn.folder.pick")) : null,
2255
+ h("button", { type: "button", className: "dsm-btn", disabled: props.busy || !picked.length, onClick: function () { props.onSubmit(entries, picked) } }, props.busy ? t("upload.importing") : t("import.submit"))))
2256
+ }
2257
+
2258
+ /**
2259
+ * 通用导出弹窗(D14):选中项 + 目标目录 → 调 `bundle-export` 打包成 zip。
2260
+ * 技能 / 子智能体 / 提示词 / 记忆四页共用,避免各写一份。
2261
+ * 只读源文件、只往用户指定目录写 zip,不改动任何插件数据。
2262
+ */
2263
+ function ExportModal(props) {
2264
+ var t = props.t
2265
+ var ds = react.useState(props.outDir || ''); var outDir = ds[0], setOutDir = ds[1]
2266
+ var ps = react.useState(false); var picking = ps[0], setPicking = ps[1]
2267
+ var items = props.items || []
2268
+ // 默认**不勾选**:导出是"把东西拿出去",一键全选最容易让人把不该带走的也带走。
2269
+ var ss = react.useState([]); var picked = ss[0], setPicked = ss[1]
2270
+ var cs = react.useState({}); var collapsed = cs[0], setCollapsed = cs[1]
2271
+ function toggle(key) {
2272
+ setPicked(function (prev) { return prev.indexOf(key) >= 0 ? prev.filter(function (k) { return k !== key }) : prev.concat([key]) })
2273
+ }
2274
+ /** 分组:`it.group` 为空 = 不分组(组头不渲染)。组内、组间都保持传入顺序。 */
2275
+ var groups = [], byGroup = {}
2276
+ items.forEach(function (it) {
2277
+ var g = String(it.group == null ? '' : it.group)
2278
+ if (!byGroup[g]) { byGroup[g] = []; groups.push({ name: g, rows: byGroup[g] }) }
2279
+ byGroup[g].push(it)
2280
+ })
2281
+ var allKeys = items.map(function (i) { return i.key })
2282
+ var allPicked = allKeys.length > 0 && picked.length === allKeys.length
2283
+ return h(Modal, { className: 'dsm-modal-list dsm-export-modal', list: true, title: props.title, closeLabel: t('btn.close'), onClose: props.onClose },
2284
+ h('div', { className: 'dsm-form dsm-export-form' },
2285
+ h('p', { className: 'dsm-help' }, t('export.hint')),
2286
+ h('div', { className: 'dsm-field' },
2287
+ h('span', { className: 'dsm-label' }, t('export.outDir')),
2288
+ h('div', { className: 'dsm-dir-row' },
2289
+ h('input', { className: 'dsm-control', type: 'text', value: outDir, placeholder: t('export.outDir.placeholder'), onChange: function (e) { setOutDir(e.target.value) } }),
2290
+ h('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: props.busy, onClick: function () { setPicking(true) } }, t('btn.openDir')))),
2291
+ // 条目列表:始终渲染(哪怕只有一条)—— 默认不勾选意味着「不列出来就没得勾」。
2292
+ items.length ? h('div', { className: 'dsm-pick-list' }, groups.map(function (g) {
2293
+ var open = collapsed[g.name] !== true
2294
+ return h('div', { key: 'g:' + g.name, className: 'dsm-pick-group' },
2295
+ g.name ? h('button', {
2296
+ type: 'button', className: 'dsm-pick-group-head', 'aria-expanded': open ? 'true' : 'false',
2297
+ onClick: function () { setCollapsed(function (prev) { var next = Object.assign({}, prev); next[g.name] = open; return next }) },
2298
+ },
2299
+ h('span', { className: 'dsm-pick-caret', 'aria-hidden': 'true' }, open ? '▾' : '▸'),
2300
+ h('span', { className: 'dsm-pick-group-name', title: g.name }, g.name),
2301
+ h('span', { className: 'dsm-pick-group-count' }, t('export.group.count', { count: g.rows.length }))) : null,
2302
+ open ? g.rows.map(function (it) {
2303
+ return pickRow({ key: it.key, checked: picked.indexOf(it.key) >= 0, name: it.name, desc: it.desc, descTitle: it.descTitle || it.desc, onChange: function () { toggle(it.key) } })
2304
+ }) : null)
2305
+ })) : h('div', { className: 'dsm-pick-empty' }, t('export.empty')),
2306
+ props.result ? h('div', { className: 'dsm-feedback' + (props.result.ok ? '' : ' dsm-error'), role: 'alert' }, props.result.text) : null),
2307
+ h('div', { className: 'dsm-modal-actions' },
2308
+ h('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: props.busy || !items.length, onClick: function () { setPicked(allPicked ? [] : allKeys.slice()) } }, allPicked ? t('export.pickNone') : t('export.pickAll')),
2309
+ h('button', { type: 'button', className: 'dsm-btn', disabled: props.busy || !picked.length || !outDir.trim(), onClick: function () { props.onSubmit(picked, outDir.trim()) } }, props.busy ? t('export.busy') : t('export.submit'))),
2310
+ picking ? h(DirPickerModal, { key: 'dir', title: t('export.pickDir'), initial: outDir, closeLabel: t('btn.close'), onClose: function () { setPicking(false) }, onPick: function (p) { setOutDir(p); setPicking(false) } }) : null)
2311
+ }
2312
+
2313
+ /**
2314
+ * 导出结果文案(四个导出页共用):成功条数 + 被跳过的项。
2315
+ *
2316
+ * 服务端把读不到的项记进 `missing` 而不是整体失败(部分成功也有价值),但**必须说出来**
2317
+ * —— 只报「已导出 N 个文件」会把静默丢项显示成完全成功(bundle 型路径曾经就是这样丢的)。
2318
+ */
2319
+ function exportResultText(t, res) {
2320
+ var text = t('export.done', { count: res.written, path: res.zipPath })
2321
+ var skipped = (res && res.missing) || []
2322
+ if (skipped.length) text += t('export.missing', { count: skipped.length, names: skipped.join(', ') })
2323
+ return text
1797
2324
  }
1798
2325
 
1799
2326
  // 目录选择器:经 host 的 dir-list 逐级浏览目录树,选中后把绝对路径回填给调用方。
@@ -1818,7 +2345,7 @@ function callApi(path, options) {
1818
2345
  function goUp() { if (!st.parent) return; setText(st.parent); load(st.parent) }
1819
2346
  function enter(path) { setText(path); load(path) }
1820
2347
  function pick() { var target = String(st.current || '').trim() || String(text || '').trim(); if (target && props.onPick) props.onPick(target) }
1821
- return h(Modal, { title: props.title || '选择文件夹', closeLabel: props.closeLabel || '关闭', onClose: props.onClose },
2348
+ return h(Modal, { title: props.title || '选择文件夹', closeLabel: props.closeLabel || t('btn.close'), onClose: props.onClose },
1822
2349
  h("div", { className: "dsm-form" },
1823
2350
  h("label", { className: "dsm-field" },
1824
2351
  h("span", { className: "dsm-label" }, '当前路径'),
@@ -1849,8 +2376,6 @@ function callApi(path, options) {
1849
2376
  var ds = react.useState(null), detail = ds[0], setDetail = ds[1];
1850
2377
  var cs = react.useState({ path: "", label: "" }), customForm = cs[0], setCustomForm = cs[1];
1851
2378
  var inflightRef = react.useRef(false);
1852
- var importInputRef = react.useRef(null);
1853
- var folderInputRef = react.useRef(null);
1854
2379
  var pickerOpenRef = react.useRef(false);
1855
2380
  function refresh(silent) { if (!silent) setSnapshot({ loading: true, error: null, data: snapshot.data }); return callApi("/state").then(function (data) { setSnapshot({ loading: false, error: null, data: data }); return data; }).catch(function (error) { setSnapshot({ loading: false, error: translateError(t, error), data: snapshot.data }); }); }
1856
2381
  react.useEffect(function () { refresh(false); }, []);
@@ -1879,10 +2404,47 @@ function callApi(path, options) {
1879
2404
  setResult({ ok: false, text: (res && res.error) || t("error.action", { error: "unknown" }) });
1880
2405
  }).catch(function (error) { setResult({ ok: false, text: t("error.action", { error: translateError(t, error) }) }); }).finally(function () { setBusy(false); });
1881
2406
  }
2407
+ // 回收站「目录」的永久删除:把这条来源记录从插件里删掉(磁盘上的文件夹与文件不动,
2408
+ // 这也正是「移除来源」与「永久删除」的区别)。与「自定义目录」的移除是同一个 op ——
2409
+ // 只有自定义来源有记录可删,按约定发现的外部来源(agents/codex/claude)删不掉,
2410
+ // 所以界面上只对 root.custom === true 的行给这个按钮。
2411
+ function submitSourceForget(key, name) {
2412
+ setBusy(true); setResult(null);
2413
+ apiCall("skill-custom-remove", { key: key }).then(function (res) {
2414
+ setModal("trash");
2415
+ if (res && res.ok) { setResult({ ok: true, text: t("result.sourceForgotten", { name: name }) }); return refresh(true); }
2416
+ setResult({ ok: false, text: t("error.action", { error: translateError(t, res) }) });
2417
+ }).catch(function (error) { setModal("trash"); setResult({ ok: false, text: t("error.action", { error: translateError(t, error) }) }); }).finally(function () { setBusy(false); });
2418
+ }
1882
2419
  function submitCreate() { post("/create", form, null).then(function (data) { setModal(null); setForm({ root: "dsh", name: "", description: "", body: "" }); setResult({ ok: true, text: t("result.created", { name: data.name }) }); }).catch(function () {}); }
1883
2420
  function selectUploadFiles(files) { pickerOpenRef.current = false; var selected = inspectUploadSelection(files); if (!selected) return; if (selected.error) { setUpload(null); setResult({ ok: false, text: translateError(t, selected.error) }); return; } setUpload(selected); setResult(null); }
1884
- function openNativePicker(ref) { if (busy || pickerOpenRef.current || !ref.current) return; pickerOpenRef.current = true; ref.current.value = ""; function release() { setTimeout(function () { pickerOpenRef.current = false; }, 0); } window.addEventListener("focus", release, { once: true }); ref.current.click(); setTimeout(function () { pickerOpenRef.current = false; }, 30000); }
1885
- function submitImport() { if (!upload) return; buildUploadPayload(upload).then(function (payload) { return post("/upload", payload, null); }).then(function (data) { var summary = summarizeImportResult(t, data); setResult({ ok: summary.ok, warning: summary.warning, text: summary.text }); if (summary.imported) { setModal(null); setUpload(null); } }).catch(function () {}); }
2421
+ /** D14:导出条目 = 按**来源文件夹**分组,组内列出该文件夹里的全部技能(含同名被遮蔽的)。 */
2422
+ function exportItems() {
2423
+ var out = [];
2424
+ for (var i = 0; i < allRoots.length; i++) {
2425
+ var root = allRoots[i], list = root.skills || [];
2426
+ var group = rootDisplayName(t, root);
2427
+ for (var j = 0; j < list.length; j++) {
2428
+ var sk = list[j];
2429
+ // key 用**条目名**(文件 / 目录名),不是 frontmatter 里的展示名:服务端按它定位源文件,
2430
+ // 用 declaredName 会在「文件名 ≠ 声明名」时找不到文件(静默丢项)。
2431
+ var nm = String(sk.name || "");
2432
+ if (!nm) continue;
2433
+ out.push({ key: String(root.key) + "/" + nm, name: String(sk.declaredName || nm), desc: sk.description || "", group: group });
2434
+ }
2435
+ }
2436
+ return out;
2437
+ }
2438
+ function doExport(names, outDir) {
2439
+ setBusy(true); setResult(null);
2440
+ apiCall("bundle-export", { kind: "skills", names: names, outDir: outDir }).then(function (res) {
2441
+ setBusy(false);
2442
+ if (res && res.ok) setResult({ ok: true, text: exportResultText(t, res) });
2443
+ else setResult({ ok: false, text: translateError(t, res) });
2444
+ }).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }); });
2445
+ }
2446
+ function submitImport(selection) { var sel = selection || upload; if (!sel) return; buildUploadPayload(sel).then(function (payload) { return post("/upload", payload, null); }).then(function (data) { var summary = summarizeImportResult(t, data); setResult({ ok: summary.ok, warning: summary.warning, text: summary.text }); if (summary.imported) { setModal(null); setUpload(null); } }).catch(function () {}); }
2447
+ var anyLocked = !!(snapshot.data && snapshot.data.anyLocked === true);
1886
2448
  var data = snapshot.data || { roots: [], trash: [], summary: { total: 0, enabled: 0, disabled: 0, issues: 0 } }, allRoots = data.roots || [], roots = visibleSkillRoots(allRoots), removedRoots = removedSkillRoots(allRoots), activeSource = roots.some(function (root) { return root.key === source; }) ? source : "";
1887
2449
  var createRoots = allRoots.filter(function (root) { return root.mutable === true; }), createOptions = createRoots.map(function (root) { return { value: root.key, label: rootDisplayName(t, root) }; }); if (!createOptions.length) createOptions.push({ value: "hub", label: t("root.hub") });
1888
2450
  // 新建技能的默认落点:hub(`tool-management/skills/`)。
@@ -1930,31 +2492,87 @@ function callApi(path, options) {
1930
2492
  function renderSkill(root, skill) {
1931
2493
  var enabled = isSkillEnabled(skill), key = skill.shadowedBy ? "status.shadowed" : skill.loadable === false ? "status.invalid" : enabled ? "status.enabled" : "status.disabled", cls = skill.shadowedBy ? "dsm-shadowed" : enabled ? "dsm-enabled" : "dsm-disabled";
1932
2494
  var canToggle = root.toggleable !== false && skill.loadable !== false;
1933
- return h("div", { key: skill.name, className: "dsm-row" }, h("div", { className: "dsm-main" }, h("div", { className: "dsm-name" }, skill.declaredName || skill.name), h("div", { className: "dsm-note" }, skill.description || t("note.missing")), skill.shadowedBy ? h("div", { className: "dsm-rule-hint" }, t("status.shadowed.hint", { name: skill.shadowedBy.name })) : null), h("div", { className: "dsm-tags" }, h("span", { className: "dsm-tag" }, rootDisplayName(t, root)), skill.preferred === true ? h("span", { className: "dsm-tag dsm-tag-on" }, t("status.preferred")) : null, !root.mutable ? h("span", { className: "dsm-tag" }, t("status.readonly")) : null, root.mutable && !root.deletable ? h("span", { className: "dsm-tag", title: t("status.notDeletable.hint") }, t("status.notDeletable")) : null), h("div", { className: "dsm-status " + cls }, t(key)), h("div", { className: "dsm-row-actions" }, canToggle && skill.shadowedBy ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", disabled: busy, title: t("btn.activate.title"), onClick: function () { activateShadowed(root, skill); } }, t("btn.activate")) : null, canToggle && !skill.shadowedBy ? h(Switch, { on: enabled, disabled: busy || root.enabled === false, label: t("skill.toggle") + " " + skill.name, onClick: function () { post(enabled ? "/disable" : "/enable", { root: root.key, name: skill.name }); } }) : null, skill.preferred === true ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", disabled: busy, title: t("btn.unprefer.title"), onClick: function () { clearPreferred(root, skill); } }, t("btn.unprefer")) : null, h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", onClick: function () { openDetail(root, skill); } }, t("btn.detail")), root.deletable === true ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", onClick: function () { setModal({ type: "trash-confirm", root: root.key, name: skill.name }); } }, t("btn.trash")) : null));
2495
+ return h("div", { key: skill.name, className: "dsm-row" }, h("div", { className: "dsm-main" }, h("div", { className: "dsm-name" }, skill.declaredName || skill.name), h("div", { className: "dsm-note" }, skill.description || t("note.missing")), skill.shadowedBy ? h("div", { className: "dsm-rule-hint" }, t("status.shadowed.hint", { name: skill.shadowedBy.name })) : null), h("div", { className: "dsm-tags" }, h("span", { className: "dsm-tag" }, rootDisplayName(t, root)), skill.preferred === true ? h("span", { className: "dsm-tag dsm-tag-on" }, t("status.preferred")) : null, !root.mutable ? h("span", { className: "dsm-tag" }, t("status.readonly")) : null, root.mutable && !root.deletable ? h("span", { className: "dsm-tag", title: t("status.notDeletable.hint") }, t("status.notDeletable")) : null), h("div", { className: "dsm-status " + cls }, t(key)), h("div", { className: "dsm-row-actions" }, canToggle && skill.shadowedBy ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", disabled: busy, title: t("btn.activate.title"), onClick: function () { activateShadowed(root, skill); } }, t("btn.activate")) : null, canToggle && !skill.shadowedBy ? h(Switch, { on: enabled, disabled: busy || root.enabled === false || anyLocked, label: t("skill.toggle") + " " + skill.name, onClick: function () { post(enabled ? "/disable" : "/enable", { root: root.key, name: skill.name }); } }) : null, skill.preferred === true ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", disabled: busy, title: t("btn.unprefer.title"), onClick: function () { clearPreferred(root, skill); } }, t("btn.unprefer")) : null, h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", onClick: function () { openDetail(root, skill); } }, t("btn.detail")), root.deletable === true ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", onClick: function () { setModal({ type: "trash-confirm", root: root.key, name: skill.name }); } }, t("btn.trash")) : null));
1934
2496
  }
1935
2497
  function renderRoot(root) {
1936
2498
  if (activeSource && activeSource !== root.key) return null;
1937
2499
  var displayName = rootDisplayName(t, root), filtered = root.skills.filter(function (skill) { return matchSkillQuery(Object.assign({}, skill, { rootKey: root.key, rootLabel: displayName }), query); }); if (query && !filtered.length) return null; var open = !!expanded[root.key] || !!query;
1938
2500
  var rootCount = root.count == null ? root.skills.length : root.count;
1939
- return h("section", { key: root.key, className: "dsm-source" }, h("div", { className: "dsm-source-head" }, h("button", { type: "button", className: "dsm-source-head-main", "aria-expanded": open, onClick: function () { setExpanded(Object.assign({}, expanded, { [root.key]: !open })); } }, h("span", { className: "dsm-source-title" }, displayName), h("span", { className: "dsm-count" }, t(countKey("summary.group", rootCount), { count: rootCount })), h("span", { className: "dsm-tag " + (root.scope === "project" ? "dsm-tag-on" : root.defaultSource === true ? "" : root.enabled ? "dsm-tag-on" : "dsm-tag-off") }, root.scope === "project" ? t("status.project") : root.defaultSource === true ? t("status.manageable") : t(root.enabled ? "status.source.on" : "status.source.off")), root.scope === "project" ? h("span", { className: "dsm-tag" }, t("status.rank", { rank: root.rank })) : null, h("span", { className: "dsm-path", title: root.path }, root.path)), root.scope !== "project" && root.defaultSource !== true && root.toggleable !== false ? h("span", { className: "dsm-source-actions" }, root.removable === true ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet dsm-btn-danger", disabled: busy, title: t("btn.source.remove.title"), onClick: function () { setModal({ type: "source-remove-confirm", key: root.key, name: displayName }); } }, t("btn.source.remove")) : null, h(Switch, { on: root.enabled, disabled: busy, label: t("source.toggle") + " " + displayName, onClick: function () { post(root.enabled ? "/source-disable" : "/source-enable", { root: root.key }); } })) : null), open ? h("div", { className: "dsm-source-body" }, filtered.length ? h(react.Fragment, null, h("div", { className: "dsm-table-head" }, h("span", null, t("table.skill")), h("span", null, t("filter.source")), h("span", null, t("table.status")), h("span", null, "")), filtered.map(function (skill) { return renderSkill(root, skill); })) : h("div", { className: "dsm-empty" }, query ? t("empty.search") : t("empty.source"))) : null);
2501
+ return h("section", { key: root.key, className: "dsm-source" }, h("div", { className: "dsm-source-head" }, h("button", { type: "button", className: "dsm-source-head-main", "aria-expanded": open, onClick: function () { setExpanded(Object.assign({}, expanded, { [root.key]: !open })); } }, h("span", { className: "dsm-source-title", title: displayName }, displayName), h("span", { className: "dsm-count" }, t(countKey("summary.group", rootCount), { count: rootCount })), h("span", { className: "dsm-tag " + (root.scope === "project" ? "dsm-tag-on" : root.defaultSource === true ? "" : root.enabled ? "dsm-tag-on" : "dsm-tag-off") }, root.scope === "project" ? t("status.project") : root.defaultSource === true ? t("status.manageable") : t(root.enabled ? "status.source.on" : "status.source.off")), root.scope === "project" ? h("span", { className: "dsm-tag" }, t("status.rank", { rank: root.rank })) : null, h("span", { className: "dsm-path", title: root.path }, root.path)), root.scope !== "project" && root.defaultSource !== true && root.toggleable !== false ? h("span", { className: "dsm-source-actions" }, root.removable === true ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet dsm-btn-danger", disabled: busy, title: t("btn.source.remove.title"), onClick: function () { setModal({ type: "source-remove-confirm", key: root.key, name: displayName }); } }, t("btn.source.remove")) : null, h(Switch, { on: root.enabled, disabled: busy || anyLocked, label: t("source.toggle") + " " + displayName, onClick: function () { post(root.enabled ? "/source-disable" : "/source-enable", { root: root.key }); } })) : null), open ? h("div", { className: "dsm-source-body" }, filtered.length ? h(react.Fragment, null, h("div", { className: "dsm-table-head" }, h("span", null, t("table.skill")), h("span", null, t("filter.source")), h("span", null, t("table.status")), h("span", null, "")), filtered.map(function (skill) { return renderSkill(root, skill); })) : h("div", { className: "dsm-empty" }, query ? t("empty.search") : t("empty.source"))) : null);
1940
2502
  }
1941
2503
 
1942
2504
  var summary = data.summary || { total: 0, enabled: 0, disabled: 0, issues: 0 };
1943
- var content = [h("style", { key: "css" }, CSS), h("div", { key: "head", className: "dsm-head" }, h("div", { className: "dsm-title-block" }, h("div", { className: "dsm-title-row" }, h("h2", { className: "dsm-title" }, t("title")), h(VersionBadgeComponent, null)), h("p", { className: "dsm-desc" }, t("desc"))), h("div", { className: "dsm-actions" }, h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: busy || snapshot.loading, onClick: function () { refresh(false); } }, t("btn.refresh")), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", onClick: openCreate }, t("btn.create")), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", onClick: function () { setResult(null); setUpload(null); setModal("import"); } }, t("btn.import")), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: busy, onClick: function () { setResult(null); setCustomForm({ path: "", label: "" }); setModal("custom-add"); } }, t("btn.custom.add")))), h("div", { key: "summary", className: "dsm-summary dsm-summary-3" }, [[summary.total, "summary.total"], [summary.enabled, "summary.enabled"], [summary.disabled, "summary.disabled"]].map(function (item) { return h("div", { key: item[1], className: "dsm-stat" }, h("strong", null, item[0]), t(countKey(item[1], item[0]), { count: item[0] }).replace(String(item[0]), "")); })), h("div", { key: "filters", className: "dsm-filters" }, h("input", { className: "dsm-control dsm-search", value: query, "aria-label": t("search"), placeholder: t("search.placeholder"), onChange: function (e) { setQuery(e.target.value); } }), h("div", { className: "dsm-source-filter" }, h(SourceSelect, { value: activeSource, options: options, onChange: setSource }))), result && modal !== "import" ? h(Notice, { key: "result", kind: result.warning ? "warn" : result.ok ? "ok" : "err", text: result.text }) : null].concat((data.warnings || []).map(function (warning, index) { return h(Notice, { key: "warning-" + index, kind: "warn", text: translateError(t, warning) }); }), [snapshot.error ? h(Notice, { key: "error", kind: "err", text: snapshot.error }) : null, snapshot.loading && !snapshot.data ? h("div", { key: "loading", className: "dsm-empty" }, t("loading")) : h("div", { key: "sources", className: "dsm-sources" }, roots.map(renderRoot)),removedRoots.length ? h("section", { key: "removed-sources", className: "dsm-source dsm-source-removed" }, h("div", { className: "dsm-source-head" }, h("div", { className: "dsm-source-head-main" }, h("span", { className: "dsm-source-title" }, t("removed.title")), h("span", { className: "dsm-count" }, t("removed.count", { count: removedRoots.length })), h("span", { className: "dsm-note" }, t("removed.hint")))), h("div", { className: "dsm-source-body" }, removedRoots.map(function (root) { var name = rootDisplayName(t, root); return h("div", { key: root.key, className: "dsm-row" }, h("div", { className: "dsm-main" }, h("div", { className: "dsm-name" }, name), h("div", { className: "dsm-note", title: root.path }, root.path)), h("div", { className: "dsm-row-actions" }, 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")))); }))) : null, h("button", { key: "trash", type: "button", className: "dsm-trash-row", onClick: function () { setModal("trash"); } }, h("span", null, t("trash.title")), h("span", { className: "dsm-trash-count" }, (data.trash || []).length))]);
2505
+ var content = [h("style", { key: "css" }, CSS), h("div", { key: "head", className: "dsm-head" }, h("div", { className: "dsm-title-block" }, h("div", { className: "dsm-title-row" }, h("h2", { className: "dsm-title" }, t("title"))), h("p", { className: "dsm-desc" }, t("desc"))), h("div", { className: "dsm-actions" }, h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: busy || snapshot.loading, onClick: function () { refresh(false); } }, (busy || snapshot.loading) ? t("btn.refreshing") : t("btn.refresh")), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: anyLocked, onClick: openCreate }, t("btn.create")), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: anyLocked, onClick: function () { setResult(null); setUpload(null); setModal("import"); } }, t("btn.import")), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: busy, onClick: function () { setResult(null); setModal("export"); } }, t("export.skills")), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: busy, onClick: function () { setResult(null); setCustomForm({ path: "", label: "" }); setModal("custom-add"); } }, t("btn.custom.add")), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", onClick: function () { setModal("trash"); } }, t("trash.btn.open")))), anyLocked ? h("div", { key: "lockbanner", className: "dsm-feedback dsm-warning", role: "status" }, t("lock.banner")) : null,
2506
+ h("div", { key: "summary", className: "dsm-summary" }, [[summary.total, "summary.total"], [summary.enabled, "summary.enabled"], [summary.issues, "summary.issues"]].map(function (item) { return h("div", { key: item[1], className: "dsm-stat" }, h("strong", null, item[0]), t(countKey(item[1], item[0]), { count: item[0] }).replace(String(item[0]), "")); })), h("div", { key: "filters", className: "dsm-filters" }, h("input", { className: "dsm-control dsm-search", value: query, "aria-label": t("search"), placeholder: t("search.placeholder"), onChange: function (e) { setQuery(e.target.value); } }), h("div", { className: "dsm-source-filter" }, h(SourceSelect, { value: activeSource, options: options, onChange: setSource }))), result && modal !== "import" ? h(Notice, { key: "result", kind: result.warning ? "warn" : result.ok ? "ok" : "err", text: result.text }) : null].concat((data.warnings || []).map(function (warning, index) { return h(Notice, { key: "warning-" + index, kind: "warn", text: translateError(t, warning) }); }), [snapshot.error ? h(Notice, { key: "error", kind: "err", text: snapshot.error }) : null, snapshot.loading && !snapshot.data ? h("div", { key: "loading", className: "dsm-empty" }, t("loading")) : h("div", { key: "sources", className: "dsm-sources" }, roots.map(renderRoot))]);
1944
2507
 
1945
2508
  if (modal === "create") content.push(h(Modal, { key: "create", title: t("create.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("div", { className: "dsm-form" }, h("label", { className: "dsm-field" }, h("span", { className: "dsm-label" }, t("create.target")), h(SourceSelect, { value: form.root, options: createOptions, onChange: function (value) { updateForm("root", value); } })), [["name", "create.name", "create.name.placeholder"], ["description", "create.description", "create.description.placeholder"]].map(function (field) { return h("label", { key: field[0], className: "dsm-field" }, h("span", { className: "dsm-label" }, t(field[1])), h("input", { className: "dsm-control", value: form[field[0]], placeholder: t(field[2]), onChange: function (e) { updateForm(field[0], e.target.value); } })); }), h("label", { className: "dsm-field" }, h("span", { className: "dsm-label" }, t("create.body")), h("textarea", { className: "dsm-control", value: form.body, placeholder: t("create.body.placeholder"), onChange: function (e) { updateForm("body", e.target.value); } })), h("p", { className: "dsm-help" }, t("create.chat.note"))), h("div", { className: "dsm-modal-actions" }, h("button", { className: "dsm-btn", disabled: busy || !form.name.trim() || !form.description.trim() || !form.body.trim(), onClick: submitCreate }, t("btn.create.now")))));
1946
- if (modal === "import") content.push(h(Modal, { key: "import", className: "dsm-modal-import", title: t("import.title"), closeLabel: t("btn.close"), onClose: function () { pickerOpenRef.current = false; setUpload(null); setResult(null); setModal(null); } },
1947
- h("input", { ref: importInputRef, className: "dsm-hidden-input", type: "file", accept: ".zip,.md", onChange: function (event) { selectUploadFiles(event.target.files); event.target.value = ""; } }),
1948
- h("input", { ref: folderInputRef, className: "dsm-hidden-input", type: "file", multiple: true, webkitdirectory: "", directory: "", onChange: function (event) { selectUploadFiles(event.target.files); event.target.value = ""; } }),
1949
- h("div", { className: "dsm-dropzone", onClick: function () { openNativePicker(importInputRef); }, onDragOver: function (event) { event.preventDefault(); event.currentTarget.classList.add("dsm-dropzone-active"); }, onDragLeave: function (event) { event.currentTarget.classList.remove("dsm-dropzone-active"); }, onDrop: function (event) { event.preventDefault(); event.currentTarget.classList.remove("dsm-dropzone-active"); droppedFiles(event.dataTransfer).then(selectUploadFiles).catch(function (error) { setResult({ ok: false, text: translateError(t, error) }); }); } },
1950
- h("span", { className: "dsm-dropzone-title" }, t("upload.drop.title")),
1951
- h("span", { className: "dsm-dropzone-copy" }, t("upload.drop.copy"))),
1952
- upload ? h("div", { className: "dsm-file", title: upload.name }, h("span", { className: "dsm-file-kind", "aria-hidden": "true" }, upload.kind === "zip" ? "ZIP" : upload.kind === "folder" ? "DIR" : "MD"), h("span", { className: "dsm-file-name" }, upload.name), h("span", { className: "dsm-file-meta" }, t(countKey("upload.selected", upload.count), { count: upload.count, size: upload.size < 1024 ? upload.size + " B" : Math.ceil(upload.size / 1024) + " KB" })), h("button", { type: "button", className: "dsm-file-remove", "aria-label": t("upload.remove"), onClick: function () { setUpload(null); } }, "×")) : null,
1953
- result ? h("div", { className: "dsm-feedback" + (result.warning ? " dsm-warning" : result.ok ? "" : " dsm-error"), role: "alert" }, result.text) : null,
1954
- h("div", { className: "dsm-upload-requirements" }, h("div", { className: "dsm-label" }, t("upload.requirements")), h("ul", null, h("li", null, t("upload.requirement.skill")), h("li", null, t("upload.requirement.frontmatter")), h("li", null, t("upload.requirement.copy")))),
1955
- h("div", { className: "dsm-modal-actions" }, h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: busy, onClick: function () { openNativePicker(folderInputRef); } }, t("btn.folder.pick")), h("button", { type: "button", className: "dsm-btn", disabled: busy || !upload, onClick: submitImport }, busy ? t("upload.importing") : t("btn.import.now")))
1956
- )); if (modal === "detail") content.push(h(Modal, { key: "detail", wide: true, title: t("detail.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, detail ? h(react.Fragment, null, h("div", { className: "dsm-detail-path" }, detail.path), h("div", { className: "dsm-modal-actions" }, h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: busy, onClick: openSource }, t("btn.open.editor"))), h("div", { className: "dsm-detail-section" }, h("div", { className: "dsm-detail-title" }, t("detail.diagnostics")), detail.diagnostics.length ? detail.diagnostics.map(function (item, index) { return h("div", { key: index, className: "dsm-diag" }, t(item.code, item.params || {})); }) : h("div", { className: "dsm-note" }, t("detail.noIssues"))), h("div", { className: "dsm-detail-section" }, h("div", { className: "dsm-detail-title" }, t("detail.frontmatter")), renderFrontmatter(t, detail.frontmatter)), h("div", { className: "dsm-detail-section" }, h("div", { className: "dsm-detail-title" }, t("detail.body")), h("pre", { className: "dsm-code" }, detail.body || ""))) : h("div", { className: "dsm-empty" }, t("loading"))));
1957
- if (modal === "trash") content.push(h(Modal, { key: "trash-modal", title: t("trash.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("div", { className: "dsm-count" }, t(countKey("trash.count", data.trash.length), { count: data.trash.length })), data.trash.length ? data.trash.map(function (item) { return h("div", { key: item.id, className: "dsm-trash-item" }, h("div", { className: "dsm-trash-main" }, h("div", { className: "dsm-name" }, item.name), h("div", { className: "dsm-note" }, t("trash.deletedAt", { time: new Date(item.deletedAt).toLocaleString() }) + " · " + t("trash.source", { source: trashRootLabel(item) }))), 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")), 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"))); }) : h("div", { className: "dsm-empty" }, t("trash.empty"))));
2509
+ // D13:导入弹窗统一用共享 ImportModal(能力取三者并集:拖拽区 + 已选文件行 +
2510
+ // 文件要求清单 + 选择文件夹 + 结果反馈)。技能页只保留自己的校验与提交逻辑。
2511
+ if (modal === "import") content.push(h(ImportModal, {
2512
+ key: "import", t: t, title: t("import.title"), accept: ".zip,.md", allowFolder: true,
2513
+ busy: busy, result: result,
2514
+ requirements: [t("upload.requirement.skill"), t("upload.requirement.frontmatter"), t("upload.requirement.copy")],
2515
+ onClose: function () { pickerOpenRef.current = false; setUpload(null); setResult(null); setModal(null); },
2516
+ onSubmit: function (entries, files) {
2517
+ var selected = inspectUploadSelection(files);
2518
+ if (!selected) return;
2519
+ if (selected.error) { setResult({ ok: false, text: translateError(t, selected.error) }); return; }
2520
+ setUpload(selected);
2521
+ submitImport(selected);
2522
+ },
2523
+ }));
2524
+ if (modal === "detail") content.push(h(Modal, { key: "detail", wide: true, title: t("detail.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, detail ? h(react.Fragment, null, h("div", { className: "dsm-detail-path" }, detail.path), h("div", { className: "dsm-modal-actions" }, h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: busy, onClick: openSource }, t("btn.open.editor"))), h("div", { className: "dsm-detail-section" }, h("div", { className: "dsm-detail-title" }, t("detail.diagnostics")), detail.diagnostics.length ? detail.diagnostics.map(function (item, index) { return h("div", { key: index, className: "dsm-diag" }, t(item.code, item.params || {})); }) : h("div", { className: "dsm-note" }, t("detail.noIssues"))), h("div", { className: "dsm-detail-section" }, h("div", { className: "dsm-detail-title" }, t("detail.frontmatter")), renderFrontmatter(t, detail.frontmatter)), h("div", { className: "dsm-detail-section" }, h("div", { className: "dsm-detail-title" }, t("detail.body")), h("pre", { className: "dsm-code" }, detail.body || ""))) : h("div", { className: "dsm-empty" }, t("loading"))));
2525
+ if (modal === "export") content.push(h(ExportModal, {
2526
+ key: "export", t: t, title: t("export.skills"),
2527
+ items: exportItems(), busy: busy, result: result,
2528
+ onClose: function () { setResult(null); setModal(null); },
2529
+ onSubmit: doExport,
2530
+ }));
2531
+ // 回收站分两部分:**目录**(被「移除来源」的目录,插件不再读取)与**技能**(移入
2532
+ // 回收站的技能)。两部分都给了「恢复」,永久删除在两边都指向"记录/文件真的没了":
2533
+ // 目录那边只删插件里的来源记录(磁盘上的文件夹一个字节都不动),技能那边才删文件。
2534
+ if (modal === "trash") {
2535
+ // 技能回收站里是**两类后果不同**的东西,弹窗里上下两块(用户要求「弹窗里嵌两个小弹窗」):
2536
+ // 上「不再读取的来源」——插件只是不再扫它,磁盘一个字节都不动(中性色,恢复读取 / 删除记录);
2537
+ // 下「回收站里的技能」——文件真的被移走了,永久删除会真删;两块的「永久删除」按钮都是红色。
2538
+ // 两块各自滚动,**大弹窗自己不滚**;两块一直在(空的那块显示自己的空态),别让人以为它没了。
2539
+ // 卡片结构复用共享回收站的 .dsm-trash-group(组头 + 计数 + 后果说明 + 行列表),观感与其它页一致。
2540
+ var skillTrashRows = data.trash || []
2541
+ var removedSourceRows = removedRoots || []
2542
+ function trashPane(key, title, count, note, danger, rows, emptyText) {
2543
+ return h("div", { key: key, className: "dsm-trash-group" + (danger ? " dsm-trash-group-danger" : "") },
2544
+ h("div", { className: "dsm-trash-group-head" },
2545
+ h("div", { className: "dsm-trash-group-head-row" },
2546
+ h("span", { className: "dsm-trash-group-title" }, title),
2547
+ h("span", { className: "dsm-count" }, count)),
2548
+ h("p", { className: "dsm-trash-group-sub" }, note)),
2549
+ h("div", { className: "dsm-trash-pane-body" }, rows.length ? rows : h("div", { className: "dsm-empty" }, emptyText)))
2550
+ }
2551
+ 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); } },
2552
+ h("div", { className: "dsm-trash-split" },
2553
+ trashPane("pane-dirs", t("trash.section.dirs"), t(countKey("trash.dirs.count", removedSourceRows.length), { count: removedSourceRows.length }), t("trash.section.dirs.sub"), false,
2554
+ removedSourceRows.map(function (root) {
2555
+ var name = rootDisplayName(t, root)
2556
+ return h("div", { key: root.key, className: "dsm-trash-item" },
2557
+ h("div", { className: "dsm-trash-main" },
2558
+ h("div", { className: "dsm-name" }, name),
2559
+ h("div", { className: "dsm-note", title: root.path }, root.path)),
2560
+ 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")),
2561
+ 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)
2562
+ }),
2563
+ t("trash.section.dirs.empty")),
2564
+ trashPane("pane-skills", t("trash.section.skills"), t(countKey("trash.count", skillTrashRows.length), { count: skillTrashRows.length }), t("trash.section.skills.sub"), false,
2565
+ skillTrashRows.map(function (item) {
2566
+ return h("div", { key: item.id, className: "dsm-trash-item" },
2567
+ h("div", { className: "dsm-trash-main" },
2568
+ h("div", { className: "dsm-name" }, item.name),
2569
+ h("div", { className: "dsm-note" }, t("trash.deletedAt", { time: new Date(item.deletedAt).toLocaleString() }))),
2570
+ 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")),
2571
+ 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")))
2572
+ }),
2573
+ t("trash.empty")))))
2574
+ }
2575
+ 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")))));
1958
2576
  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(function () {}); } }, t("btn.trash")))));
1959
2577
  if (modal && modal.type === "delete-confirm") content.push(h(Modal, { key: "delete-confirm", title: t("confirm.delete.title"), closeLabel: t("btn.close"), onClose: function () { setModal("trash"); } }, h("p", { className: "dsm-desc" }, t("confirm.delete.desc", { name: modal.name })), h("div", { className: "dsm-modal-actions" }, h("button", { className: "dsm-btn dsm-btn-danger", disabled: busy, onClick: function () { post("/trash-delete", { id: modal.id }, "result.deleted", { name: modal.name }).then(function () { setModal("trash"); }).catch(function () {}); } }, t("btn.delete.forever")))));
1960
2578
  if (modal === "custom-add") content.push(h(Modal, { key: "custom-add", title: t("custom.add.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("div", { className: "dsm-form" }, h("label", { className: "dsm-field" }, h("span", { className: "dsm-label" }, t("custom.add.path")), h("div", { className: "dsm-dir-row" }, h("input", { className: "dsm-control", value: customForm.path, placeholder: t("custom.add.path.placeholder"), onChange: function (e) { setCustomForm(Object.assign({}, customForm, { path: e.target.value })); } }), h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", disabled: busy, onClick: openCustomDir, title: t("custom.add.openDir.title") }, t("btn.openDir")))), h("label", { className: "dsm-field" }, h("span", { className: "dsm-label" }, t("custom.add.label")), h("input", { className: "dsm-control", value: customForm.label, placeholder: t("custom.add.label.placeholder"), onChange: function (e) { setCustomForm(Object.assign({}, customForm, { label: e.target.value })); } })), h("p", { className: "dsm-help" }, t("custom.add.help")), result ? h("div", { className: "dsm-feedback" + (result.warning ? " dsm-warning" : result.ok ? "" : " dsm-error"), role: "alert" }, result.text) : null), customForm.picking ? h(DirPickerModal, { key: "custom-dir-picker", title: t("custom.add.picker.title"), initial: String(customForm.path || ""), closeLabel: t("btn.close"), onClose: function () { setCustomForm(Object.assign({}, customForm, { picking: false })); }, onPick: function (path) { setCustomForm(Object.assign({}, customForm, { path: path, picking: false })); } }) : null, h("div", { className: "dsm-modal-actions" }, h("button", { type: "button", className: "dsm-btn", disabled: busy || !String(customForm.path || "").trim(), onClick: submitCustomAdd }, t("custom.add.submit")))));
@@ -1984,7 +2602,6 @@ function callApi(path, options) {
1984
2602
  // 导入对话弹窗:null = 关闭;{ busy, error, result, cwd } = 打开。
1985
2603
  var impState = React.useState(null)
1986
2604
  var imp = impState[0], setImp = impState[1]
1987
- var importInputRef = React.useRef(null)
1988
2605
  // 导出对话弹窗:null = 关闭;{ busy, error, result, outDir, format, wsFilter, selectedIds } = 打开。
1989
2606
  var expState = React.useState(null)
1990
2607
  var exp = expState[0], setExp = expState[1]
@@ -2217,13 +2834,6 @@ function callApi(path, options) {
2217
2834
  }
2218
2835
  reader.readAsText(file)
2219
2836
  }
2220
- function onImportDrop(e) {
2221
- if (e && e.preventDefault) e.preventDefault()
2222
- if (imp && imp.busy) return
2223
- droppedFiles(e && e.dataTransfer).then(function (files) {
2224
- if (files && files.length) doImportFile(files[0])
2225
- }).catch(function () {})
2226
- }
2227
2837
  function openImportPicker() {
2228
2838
  setImp({ busy: false, error: null, result: null, cwd: '' })
2229
2839
  }
@@ -2380,8 +2990,8 @@ function callApi(path, options) {
2380
2990
  React.createElement('span', { className: 'dsm-source-title dsm-hist-group-title', title: g.title }, g.title),
2381
2991
  React.createElement('span', { className: 'dsm-count' }, t('hist.group.count', { count: g.items.length }))),
2382
2992
  detached ? React.createElement('div', { className: 'dsm-hist-group-extra' },
2383
- React.createElement('span', { className: 'dsm-tag' + (g.registered ? '' : ' dsm-tag-off'), title: g.registered ? t('hist.group.removed.hint') : t('hist.group.unregistered.hint') },
2384
- g.registered ? t('hist.group.removed') : t('hist.group.unregistered')),
2993
+ React.createElement('span', { className: 'dsm-tag' + (g.registered ? '' : ' dsm-tag-off'), title: t('hist.group.removed.hint') },
2994
+ t('hist.group.removed')),
2385
2995
  g.canRegister ? React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, title: t('hist.btn.registerWs.hint'), onClick: function () { setModal({ type: 'register', path: g.path, title: g.title }) } }, t('hist.btn.registerWs')) : null) : null,
2386
2996
  g.path ? React.createElement('span', { className: 'dsm-path', title: g.path }, g.path) : null),
2387
2997
  open ? React.createElement('div', { className: 'dsm-source-body' }, g.items.map(renderRow)) : null)
@@ -2391,11 +3001,10 @@ function callApi(path, options) {
2391
3001
  React.createElement('div', { className: 'dsm-head' },
2392
3002
  React.createElement('div', { className: 'dsm-title-block' },
2393
3003
  React.createElement('div', { className: 'dsm-title-row' },
2394
- React.createElement('h2', { className: 'dsm-title' }, t('tabs.sessions')),
2395
- VersionBadgeComponent ? React.createElement(VersionBadgeComponent, null) : null),
3004
+ React.createElement('h2', { className: 'dsm-title' }, t('tabs.sessions'))),
2396
3005
  React.createElement('p', { className: 'dsm-desc' }, t('sessions.desc'))),
2397
3006
  React.createElement('div', { className: 'dsm-actions' },
2398
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: !filtered.length, onClick: function () { toggleAllVisible(visibleIds) } }, allVisible ? t('hist.deselectAll') : t('hist.selectAll')),
3007
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: data.loading, onClick: function () { refresh() } }, data.loading ? t('btn.refreshing') : t('btn.refresh')),
2399
3008
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: openImportPicker }, t('hist.btn.import')),
2400
3009
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: openExportPicker }, t('hist.btn.export')),
2401
3010
  selected.size > 0 ? React.createElement('div', { className: 'dsm-hist-batch' },
@@ -2403,11 +3012,13 @@ function callApi(path, options) {
2403
3012
  React.createElement('div', { className: 'dsm-hist-batch-actions' },
2404
3013
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { doUnarchiveBatch() } }, t('hist.btn.restoreSelected', { count: selected.size })),
2405
3014
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { setModal({ type: 'deleteBatch', count: selected.size }) } }, t('hist.btn.deleteSelected', { count: selected.size })))) : null)),
2406
- React.createElement('div', { className: 'dsm-summary dsm-summary-3' },
2407
- [[data.items.length, t('hist.stat.archived')], [grouped ? groups.length : '—', t('hist.stat.projects')], [selected.size, t('hist.stat.selected')]].map(function (item) {
2408
- return React.createElement('div', { key: item[1], className: 'dsm-stat' },
2409
- React.createElement('strong', null, item[0]), item[1])
2410
- })),
3015
+ React.createElement('div', { className: 'dsm-stat-row' },
3016
+ React.createElement('div', { className: 'dsm-summary' },
3017
+ [['archived', data.items.length, t('hist.stat.archived')], ['projects', grouped ? groups.length : '—', t('hist.stat.projects')], ['retention', data.retentionDays ? String(data.retentionDays) : '∞', t('hist.stat.retention')]].map(function (item) {
3018
+ return React.createElement('div', { key: item[0], className: 'dsm-stat' },
3019
+ React.createElement('strong', null, item[1]), item[2])
3020
+ })),
3021
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: !filtered.length, onClick: function () { toggleAllVisible(visibleIds) } }, allVisible ? t('hist.deselectAll') : t('hist.selectAll'))),
2411
3022
  React.createElement('div', { className: 'dsm-filters' },
2412
3023
  React.createElement('input', { className: 'dsm-control dsm-search', type: 'text', placeholder: t('hist.search.placeholder'), value: query, onChange: function (e) { setQuery(e.target.value) } }),
2413
3024
  React.createElement('div', { className: 'dsm-source-filter' },
@@ -2420,15 +3031,15 @@ function callApi(path, options) {
2420
3031
  : filtered.length === 0 ? React.createElement('div', { className: 'dsm-empty' }, query ? t('hist.empty.search') : t('hist.empty.none'))
2421
3032
  : React.createElement('div', { className: 'dsm-sources' },
2422
3033
  grouped ? groups.map(renderGroup) : React.createElement('div', { className: 'dsm-source' }, filtered.map(renderRow))),
2423
- modal && modal.type === 'delete' ? React.createElement(Modal, { key: 'del', title: t('hist.purge.title'), closeLabel: t('btn.cancel'), onClose: function () { setModal(null) } },
3034
+ modal && modal.type === 'delete' ? React.createElement(Modal, { key: 'del', title: t('hist.purge.title'), closeLabel: t('btn.close'), onClose: function () { setModal(null) } },
2424
3035
  React.createElement('p', { className: 'dsm-help' }, t('hist.purge.one', { title: modal.title })),
2425
3036
  React.createElement('div', { className: 'dsm-modal-actions' },
2426
3037
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { doDelete(modal.sessionId) } }, t('hist.btn.confirmPurge')))) : null,
2427
- modal && modal.type === 'deleteBatch' ? React.createElement(Modal, { key: 'delb', title: t('hist.purge.title'), closeLabel: t('btn.cancel'), onClose: function () { setModal(null) } },
3038
+ modal && modal.type === 'deleteBatch' ? React.createElement(Modal, { key: 'delb', title: t('hist.purge.title'), closeLabel: t('btn.close'), onClose: function () { setModal(null) } },
2428
3039
  React.createElement('p', { className: 'dsm-help' }, t('hist.purge.batch', { count: modal.count })),
2429
3040
  React.createElement('div', { className: 'dsm-modal-actions' },
2430
3041
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { doDeleteBatch() } }, t('hist.btn.confirmPurge')))) : null,
2431
- modal && modal.type === 'register' ? React.createElement(Modal, { key: 'reg', title: t('hist.register.title'), closeLabel: t('btn.cancel'), onClose: function () { setModal(null) } },
3042
+ modal && modal.type === 'register' ? React.createElement(Modal, { key: 'reg', title: t('hist.register.title'), closeLabel: t('btn.close'), onClose: function () { setModal(null) } },
2432
3043
  React.createElement('p', { className: 'dsm-help' }, t('hist.register.body', { title: modal.title })),
2433
3044
  React.createElement('p', { className: 'dsm-help dsm-hist-register-path' }, String(modal.path || '')),
2434
3045
  React.createElement('div', { className: 'dsm-modal-actions' },
@@ -2441,18 +3052,24 @@ function callApi(path, options) {
2441
3052
  (modal.workspace && modal.workspace.attachSkipped && modal.workspace.attachSkipped.length)
2442
3053
  ? React.createElement('p', { className: 'dsm-help dsm-warning' }, t('hist.register.done.skipped', { count: modal.workspace.attachSkipped.length, reason: String(modal.workspace.attachSkipped[0].reason || '') }))
2443
3054
  : null) : null,
2444
- imp ? React.createElement(Modal, { key: 'imp', className: 'dsm-modal-import', title: t('hist.btn.import'), closeLabel: t('btn.close'), onClose: function () { setImp(null) } },
2445
- React.createElement('p', { className: 'dsm-help' }, t('hist.import.hint')),
2446
- React.createElement('div', { className: 'dsm-field' },
2447
- React.createElement('label', { className: 'dsm-label' }, t('hist.import.cwd')),
2448
- React.createElement('input', { className: 'dsm-control', type: 'text', placeholder: t('hist.import.cwd.placeholder'), value: imp.cwd || '', onChange: function (e) { setImp(Object.assign({}, imp, { cwd: e.target.value })) } })),
2449
- React.createElement('div', { className: 'dsm-dropzone', onDragOver: function (e) { if (e.preventDefault) e.preventDefault() }, onDrop: onImportDrop, onClick: function () { if (importInputRef.current) importInputRef.current.click() } },
2450
- React.createElement('div', { className: 'dsm-dropzone-title' }, t('hist.import.drop')),
2451
- React.createElement('div', { className: 'dsm-dropzone-copy' }, '.jsonl / .json / .md / .txt')),
2452
- React.createElement('input', { type: 'file', ref: importInputRef, className: 'dsm-hidden-input', accept: '.jsonl,.json,.md,.markdown,.txt', onChange: function (e) { var f = e.target && e.target.files && e.target.files[0]; if (f) doImportFile(f) } }),
2453
- imp.error ? React.createElement('div', { className: 'dsm-feedback dsm-error' }, String(imp.error)) : null,
2454
- imp.result ? React.createElement('div', { className: 'dsm-feedback' }, t('hist.import.done', { id: imp.result.sessionId, count: imp.result.count })) : null) : null,
2455
- exp ? React.createElement(Modal, { key: 'exp', className: 'dsm-modal-import', title: t('hist.btn.export'), closeLabel: t('btn.close'), onClose: function () { setExp(null) } },
3055
+ // D13:会话页也改用共享 ImportModal(能力取三者并集)。差异只在 extra 区
3056
+ // (项目目录)与提交回调:选完文件直接导入,没有二次确认。
3057
+ imp ? React.createElement(ImportModal, {
3058
+ key: 'imp',
3059
+ t: t,
3060
+ title: t('hist.btn.import'),
3061
+ accept: '.jsonl,.json,.md,.markdown,.txt',
3062
+ busy: imp.busy === true,
3063
+ requirements: [t('upload.requirement.session.1'), t('upload.requirement.session.2'), t('upload.requirement.session.3')],
3064
+ result: imp.error ? { ok: false, text: String(imp.error) } : (imp.result ? { ok: true, text: t('hist.import.done', { id: imp.result.sessionId, count: imp.result.count }) } : null),
3065
+ extra: React.createElement('div', null,
3066
+ React.createElement('div', { className: 'dsm-field' },
3067
+ React.createElement('label', { className: 'dsm-label' }, t('hist.import.cwd')),
3068
+ React.createElement('input', { className: 'dsm-control', type: 'text', placeholder: t('hist.import.cwd.placeholder'), value: imp.cwd || '', onChange: function (e) { setImp(Object.assign({}, imp, { cwd: e.target.value })) } }))),
3069
+ onClose: function () { setImp(null) },
3070
+ onSubmit: function (entries, files) { doImportFile(files[0]) },
3071
+ }) : null,
3072
+ exp ? React.createElement(Modal, { key: 'exp', className: 'dsm-modal-list dsm-export-modal', list: true, title: t('hist.btn.export'), closeLabel: t('btn.close'), onClose: function () { setExp(null) } },
2456
3073
  React.createElement('p', { className: 'dsm-help' }, t('hist.export.hint')),
2457
3074
  React.createElement('div', { className: 'dsm-field' },
2458
3075
  React.createElement('label', { className: 'dsm-label' }, t('hist.export.dir')),
@@ -2469,19 +3086,37 @@ function callApi(path, options) {
2469
3086
  React.createElement('div', { className: 'dsm-field' },
2470
3087
  React.createElement('label', { className: 'dsm-label' }, t('hist.export.workspace')),
2471
3088
  React.createElement(SourceSelect, { options: exportWsOptions, value: exp.wsFilter, onChange: function (v) { setExp(Object.assign({}, exp, { wsFilter: v, selectedIds: new Set() })) } })),
2472
- React.createElement('div', { className: 'dsm-field' },
3089
+ React.createElement('div', { className: 'dsm-field dsm-export-body' },
2473
3090
  React.createElement('div', { className: 'dsm-label' }, t('hist.export.sessions', { count: exportItems.length })),
2474
- React.createElement('div', { className: 'dsm-source' },
3091
+ React.createElement('div', { className: 'dsm-source dsm-pick-list' },
2475
3092
  exp.loading ? React.createElement('div', { className: 'dsm-empty' }, t('hist.export.loading'))
2476
- : exportItems.length ? exportItems.map(function (it) {
2477
- return React.createElement('label', { key: it.sessionId, className: 'dsm-hist-row' },
2478
- React.createElement('input', { type: 'checkbox', className: 'dsm-hist-check', checked: exp.selectedIds.has(it.sessionId), onChange: function () { toggleExportSelect(it.sessionId) } }),
2479
- React.createElement('div', { className: 'dsm-hist-main' },
2480
- React.createElement('div', { className: 'dsm-hist-title' }, it.title || ('Session ' + shortId(it.sessionId))),
2481
- it.cwd ? React.createElement('div', { className: 'dsm-hist-cwd' + (it.cwdMissing ? ' dsm-hist-cwd-missing' : ''), title: (it.cwdMissing ? t('hist.cwd.missing') : '') + it.cwd }, (it.cwdMissing ? '⚠ ' : '') + it.cwd) : null),
2482
- React.createElement('div', { className: 'dsm-hist-actions' },
2483
- React.createElement('span', { className: 'dsm-tag' + (it.archived ? '' : ' dsm-tag-on') }, it.archived ? t('hist.tag.archived') : t('hist.tag.live'))))
2484
- }) : React.createElement('div', { className: 'dsm-empty' }, t('hist.export.empty')))),
3093
+ : exportItems.length ? (function () {
3094
+ // 按**工作区目录**分组:不同文件夹的会话分开列,组头可折叠。
3095
+ var groups = [], byCwd = {}
3096
+ exportItems.forEach(function (it) {
3097
+ var ck = String(it.cwd || '')
3098
+ if (!byCwd[ck]) { byCwd[ck] = []; groups.push({ cwd: ck, rows: byCwd[ck] }) }
3099
+ byCwd[ck].push(it)
3100
+ })
3101
+ return groups.map(function (g) {
3102
+ var key = 'exp:' + g.cwd
3103
+ var open = collapsed[key] !== true
3104
+ return React.createElement('div', { key: key, className: 'dsm-pick-group' },
3105
+ React.createElement('button', { type: 'button', className: 'dsm-pick-group-head', 'aria-expanded': open ? 'true' : 'false', onClick: function () { toggleGroupCollapse(key) } },
3106
+ React.createElement('span', { className: 'dsm-pick-caret', 'aria-hidden': 'true' }, open ? '▾' : '▸'),
3107
+ React.createElement('span', { className: 'dsm-pick-group-name', title: g.cwd || t('hist.ungrouped') }, g.cwd || t('hist.ungrouped')),
3108
+ React.createElement('span', { className: 'dsm-pick-group-count' }, t('export.group.count', { count: g.rows.length }))),
3109
+ open ? g.rows.map(function (it) {
3110
+ return React.createElement('label', { key: it.sessionId, className: 'dsm-hist-row' },
3111
+ React.createElement('input', { type: 'checkbox', className: 'dsm-hist-check', checked: exp.selectedIds.has(it.sessionId), onChange: function () { toggleExportSelect(it.sessionId) } }),
3112
+ React.createElement('div', { className: 'dsm-hist-main' },
3113
+ React.createElement('div', { className: 'dsm-hist-title' }, it.title || ('Session ' + shortId(it.sessionId))),
3114
+ it.cwdMissing ? React.createElement('div', { className: 'dsm-hist-cwd dsm-hist-cwd-missing', title: t('hist.cwd.missing') + it.cwd }, '⚠ ' + it.cwd) : null),
3115
+ React.createElement('div', { className: 'dsm-hist-actions' },
3116
+ React.createElement('span', { className: 'dsm-tag' + (it.archived ? '' : ' dsm-tag-on') }, it.archived ? t('hist.tag.archived') : t('hist.tag.live'))))
3117
+ }) : null)
3118
+ })
3119
+ })() : React.createElement('div', { className: 'dsm-empty' }, t('hist.export.empty')))),
2485
3120
  exp.error ? React.createElement('div', { className: 'dsm-feedback dsm-error' }, String(exp.error)) : null,
2486
3121
  exp.result ? React.createElement('div', { className: 'dsm-feedback' },
2487
3122
  exp.result.exported
@@ -2617,6 +3252,34 @@ function callApi(path, options) {
2617
3252
  var presetOptions = pos[0], setPresetOptions = pos[1]
2618
3253
  var dts = React.useState(null)
2619
3254
  var drillTools = dts[0], setDrillTools = dts[1]
3255
+ // 场景回收站(记录 + 档案):null = 关闭;{loading, error, entries}
3256
+ var sts = React.useState(null)
3257
+ var trash = sts[0], setTrash = sts[1]
3258
+ var stb = React.useState(false)
3259
+ var trashBusy = stb[0], setTrashBusy = stb[1]
3260
+ function loadTrash(keepOpen) {
3261
+ setTrash(Object.assign({ loading: true, error: null, entries: [] }, keepOpen ? trash || {} : {}))
3262
+ apiCall('scene-trash-list', {}).then(function (res) {
3263
+ if (res && res.ok) setTrash({ loading: false, error: null, entries: res.trash || [] })
3264
+ else setTrash({ loading: false, error: (res && res.error) || t('trash.loadFailed'), entries: [] })
3265
+ }).catch(function (e) { setTrash({ loading: false, error: String((e && e.message) || e), entries: [] }) })
3266
+ }
3267
+ function restoreScene(item) {
3268
+ setTrashBusy(true)
3269
+ apiCall('scene-trash-restore', { id: item.id }).then(function (res) {
3270
+ setTrashBusy(false)
3271
+ if (res && res.ok) { setResult({ ok: true, text: t('trash.restored', { name: item.name }) }); refresh(true); loadTrash(true) }
3272
+ else setTrash(function (cur) { return Object.assign({}, cur, { error: (res && res.error) || t('mcp.msg.failed') }) })
3273
+ }).catch(function (e) { setTrashBusy(false); setTrash(function (cur) { return Object.assign({}, cur, { error: String((e && e.message) || e) }) }) })
3274
+ }
3275
+ function purgeScene(item) {
3276
+ setTrashBusy(true)
3277
+ apiCall('scene-trash-delete', { id: item.id }).then(function (res) {
3278
+ setTrashBusy(false)
3279
+ if (res && res.ok) { setResult({ ok: true, text: t('trash.purged', { name: item.name }) }); loadTrash(true) }
3280
+ else setTrash(function (cur) { return Object.assign({}, cur, { error: (res && res.error) || t('mcp.msg.failed') }) })
3281
+ }).catch(function (e) { setTrashBusy(false); setTrash(function (cur) { return Object.assign({}, cur, { error: String((e && e.message) || e) }) }) })
3282
+ }
2620
3283
  React.useEffect(function () { if (!result || result.ok !== true) return undefined; var timer = setTimeout(function () { setResult(null) }, 2600); return function () { clearTimeout(timer) } }, [result])
2621
3284
  function refresh(silent) {
2622
3285
  if (!silent) setData(function (prev) { return Object.assign({}, prev, { loading: true, error: null }) })
@@ -2657,10 +3320,30 @@ function callApi(path, options) {
2657
3320
  else setResult({ ok: false, text: translateError(t, res) })
2658
3321
  }).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
2659
3322
  }
3323
+ /**
3324
+ * 场景开关(P6):**唯一入口**。
3325
+ *
3326
+ * 开 = 进入该场景:应用档案(MCP/技能双向切换)+ 启用场景(提示词绑定生效 + 记忆范围收窄);
3327
+ * 关 = 回到全局默认:恢复档案快照 + 取消场景启用。
3328
+ *
3329
+ * ⚠️ 核心约束:`mode.scene` 与 `active` **必须恒等**。此前开关只写 `active`、
3330
+ * 「切入此模式」按钮只写 `mode.scene`,两者互不知情 —— 于是「拨开关不应用档案」与
3331
+ * 「顶部横幅永远不显示」同时发生。现在两条路径都走 enterMode / exitMode。
3332
+ */
2660
3333
  function toggleScene(scene) {
2661
3334
  if (busy || scene.shared || scene.global) return
2662
- // 单选:点已启用的 = 关闭;点未启用的 = 只启用它(其余自动关掉,服务端也只接受一个)。
2663
- setActiveScenes(scene.active === true ? [] : [scene.name])
3335
+ if (scene.active === true) exitMode()
3336
+ else enterMode(scene.name)
3337
+ }
3338
+ /** 场景锁定(v0.8):锁上后五个管理域整体只读(场景页 + 各功能页);启停不受影响。 */
3339
+ function toggleSceneLock(scene) {
3340
+ if (busy || scene.shared || scene.global) return
3341
+ setBusy(true)
3342
+ apiCall('rules-scene-lock', { scene: scene.name, locked: scene.locked !== true }).then(function (res) {
3343
+ setBusy(false)
3344
+ if (res && res.ok) refresh(true)
3345
+ else setResult({ ok: false, text: translateError(t, res) })
3346
+ }).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
2664
3347
  }
2665
3348
  /**
2666
3349
  * 提示词预设候选(打开表单时拉一次;失败就只留「不绑定」)。
@@ -2700,8 +3383,13 @@ function callApi(path, options) {
2700
3383
  if (!isValidSceneName(name)) { setSceneForm(Object.assign({}, sceneForm, { error: t('error.rules.invalidGroup') })); return }
2701
3384
  var description = String(sceneForm.description || '').trim()
2702
3385
  var prompt = String(sceneForm.prompt || '').trim()
3386
+ // 编辑时表单里的 name 可能改过:改名走 nextName,服务端按 modal.name(原名)定位、改完再落新名。
3387
+ var originalName = isEdit ? String((modal && modal.name) || name) : name
2703
3388
  setBusy(true)
2704
- apiCall(isEdit ? 'rules-update-scene' : 'rules-create-scene', { name: name, description: description, prompt: prompt }).then(function (res) {
3389
+ var payload = isEdit
3390
+ ? { name: originalName, nextName: name, description: description, prompt: prompt }
3391
+ : { name: name, description: description, prompt: prompt }
3392
+ apiCall(isEdit ? 'rules-update-scene' : 'rules-create-scene', payload).then(function (res) {
2705
3393
  setBusy(false)
2706
3394
  if (res && res.ok) {
2707
3395
  setModal(null)
@@ -2715,8 +3403,17 @@ function callApi(path, options) {
2715
3403
  setBusy(true)
2716
3404
  apiCall('rules-remove-scene', { name: name }).then(function (res) {
2717
3405
  setBusy(false); setModal(null)
2718
- if (res && res.ok) { setResult({ ok: true, text: t('memory.result.sceneRemoved', { name: name }) }); refresh(true) }
2719
- else setResult({ ok: false, text: translateError(t, res) })
3406
+ if (res && res.ok) {
3407
+ var moved = Number((res && res.movedFiles) || 0)
3408
+ // 如实说清「一起删了多少条记忆」:删除场景现在会带走它下面的全部记忆。
3409
+ setResult({
3410
+ ok: true,
3411
+ text: moved > 0
3412
+ ? t('memory.result.sceneRemoved.withMemories', { name: name, count: moved })
3413
+ : t('memory.result.sceneRemoved', { name: name }),
3414
+ })
3415
+ refresh(true)
3416
+ } else setResult({ ok: false, text: translateError(t, res) })
2720
3417
  }).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
2721
3418
  }
2722
3419
  // ── 档案(mcp 两级 / skills / subagents 三段自由搭配)──
@@ -2737,6 +3434,8 @@ function callApi(path, options) {
2737
3434
  },
2738
3435
  inventory: {
2739
3436
  mcpServers: inv.mcpServers || [], skills: inv.skills || [], subagents: inv.subagents || [], tools: inv.tools || [],
3437
+ // 模式名与顺序(标准 → PTC → 极简 → 创造 → 自建):人设行的限制摘要按这个顺序排。
3438
+ presets: inv.presets || [],
2740
3439
  // 记忆段的数据源(宿主 scene-inventory 回传;老宿主缺失时退化为空列表而不是崩)。
2741
3440
  // 只留扁平 memories——记忆段现在按「记忆所属场景」自己筛,不再需要场景清单。
2742
3441
  memories: inv.memories || [],
@@ -2775,11 +3474,11 @@ function callApi(path, options) {
2775
3474
  setSections(Object.assign({}, sections, { mcp: mcp }))
2776
3475
  }
2777
3476
  /** 档案各段共用的段头动作(包装共享的 segActions,把 busy / 文案在这里补上)。 */
2778
- function archiveSegActions(defined, addLabel, onAdd, onRemove, onAll, onClear) {
3477
+ function archiveSegActions(defined, addLabel, onAdd, onRemove, onAll, onClear, allChecked) {
2779
3478
  return segActions({
2780
3479
  defined: defined, busy: busy, addLabel: addLabel,
2781
3480
  selectAllLabel: t('scenes.seg.selectAll'), clearLabel: t('scenes.seg.clear'), removeLabel: t('memory.archive.removeSection'),
2782
- onAdd: onAdd, onRemove: onRemove, onAll: onAll, onClear: onClear,
3481
+ onAdd: onAdd, onRemove: onRemove, onAll: onAll, onClear: onClear, allChecked: allChecked,
2783
3482
  })
2784
3483
  }
2785
3484
  /**
@@ -2790,74 +3489,80 @@ function callApi(path, options) {
2790
3489
  return segFoot(defined, count, emptyLabel || t('memory.archive.emptySection'))
2791
3490
  }
2792
3491
 
2793
- // ── 段 4:记忆 ─────────────────────────────────────────────────────
2794
- // **只列被编辑场景自己的记忆**,与 MCP / 技能 / 子智能体三段同构:标题栏 + 一排勾选行,
2795
- // 没有场景卡片、没有「选记忆」钻取。两个理由:
2796
- // ① 语义(实测):宿主的 `memoryAllowed(archives, file.scene, id)` 按**记忆所属场景**取档案
2797
- // (`src/rules/archive.ts:135`,渲染调用点 `src/rules/service.ts:1044`)——A 场景档案里的
2798
- // memories 段只能门控 A 自己的记忆。勾别的场景(含保留场景 `global`)**完全不生效**,
2799
- // 列出来就是陷阱。回归测试 `hub-layout.test.mjs`「保留场景 global 的记忆不受其它场景的勾选段影响」。
2800
- // ② 用户裁定:「其他场景的记忆也不需要显示全局」——全局恒定注入,本来也不需要在这里配置。
2801
- // 勾选语义与其余段一致:段已定义 → 没勾的该场景记忆不注入;段未定义 = 不碰。文件一律不动。
3492
+ // ── 段 4:记忆(P5:单一真相源 = `rules[*].enabled`)────────────────────
3493
+ // 这个段**没有自己的「段定义」概念**:勾选状态**就是**记忆自身的 `enabled`,
3494
+ // 与记忆页同一个写入口(`rules-toggle`)。于是「记忆页开了 → 档案页显示开启」
3495
+ // 天然成立,不存在两套状态、也不需要双向同步代码。
3496
+ //
3497
+ // 勾选**立即提交**(不等「保存到场景」):它是全局状态,不属于场景档案。
3498
+ //
3499
+ // **只列被编辑场景自己的记忆**:保留场景 `global` 恒定注入(任何对话都生效),
3500
+ // 不需要在这里配置,列出来只会让人以为「不勾就不注入」。
2802
3501
  function sceneMemories(sceneName) {
2803
3502
  return sceneMemoriesOf(modal.inventory.memories, sceneName)
2804
3503
  }
2805
- /** 「全选」= 勾上本场景的全部记忆。 */
3504
+ /** 「全选 / 全不选」作用的 id 集合 = 本场景的全部记忆。 */
2806
3505
  function allMemoryIds() {
2807
3506
  return sceneMemories(modal.name).map(function (m) { return String(m.id) })
2808
3507
  }
2809
- /** 「添加记忆段」的默认勾选:本场景里**已启用**的那几条。 */
2810
- function memPreset() {
2811
- return memDefaultPickIds(modal.inventory.memories, data.rules, modal.name)
2812
- }
2813
- function memCheckedCount() {
2814
- var sections = modalSections()
2815
- return sections.memories ? sections.memories.length : 0
3508
+ /** 记忆是否开启 —— 直接读 `rules[*].enabled`(单一真相源)。 */
3509
+ function memoryEnabled(id) {
3510
+ var rule = (data.rules || []).find(function (r) { return r.id === id })
3511
+ return !!(rule && rule.enabled !== false)
2816
3512
  }
2817
3513
  function toggleMemory(id) {
2818
- var sections = modalSections(); if (!sections) return
2819
- var list = (sections.memories || []).slice()
2820
- var i = list.indexOf(id)
2821
- if (i >= 0) list.splice(i, 1); else list.push(id)
2822
- setSections(Object.assign({}, sections, { memories: list }))
3514
+ var next = !memoryEnabled(id)
3515
+ setBusy(true)
3516
+ apiCall('rules-toggle', { id: id, enabled: next }).then(function (res) {
3517
+ setBusy(false)
3518
+ if (res && res.ok) refresh(true)
3519
+ else setResult({ ok: false, text: translateError(t, res) })
3520
+ }).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
3521
+ }
3522
+ /** 全选 / 全不选:批量写 `enabled`(顺序提交,失败即停并如实报错)。 */
3523
+ function setAllMemories(on) {
3524
+ var ids = allMemoryIds()
3525
+ if (!ids.length) return
3526
+ setBusy(true)
3527
+ Promise.all(ids.map(function (id) { return apiCall('rules-toggle', { id: id, enabled: on }) }))
3528
+ .then(function () { setBusy(false); refresh(true) })
3529
+ .catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
2823
3530
  }
2824
3531
  function memoriesSeg() {
2825
- var sections = modalSections()
2826
- var defined = !!sections.memories
2827
3532
  var own = sceneMemories(modal.name)
2828
3533
  var q = String(modal.memQuery || '').trim().toLowerCase()
2829
3534
  var items = q ? own.filter(function (m) {
2830
3535
  return String(m.name).toLowerCase().indexOf(q) >= 0
2831
3536
  || String(m.description || '').toLowerCase().indexOf(q) >= 0
2832
3537
  }) : own
3538
+ var enabledCount = own.filter(function (m) { return memoryEnabled(String(m.id)) }).length
2833
3539
  return seg({
2834
3540
  title: t('memory.archive.memories'),
2835
- count: defined ? t('scenes.seg.checked', { checked: memCheckedCount(), total: own.length }) : t('scenes.archive.sectionOff'),
2836
- actions: archiveSegActions(defined, t('memory.archive.addMemories'),
2837
- function () { setSections(Object.assign({}, modalSections(), { memories: memPreset() })) },
2838
- function () { var s = Object.assign({}, modalSections()); delete s.memories; setSections(s) },
2839
- function () { setSections(Object.assign({}, modalSections(), { memories: allMemoryIds() })) },
2840
- function () { setSections(Object.assign({}, modalSections(), { memories: [] })) }),
2841
- body: defined
2842
- ? React.createElement('div', { className: 'dsm-seg-body' },
2843
- segFilter(modal.memQuery, function (v) { setModal(Object.assign({}, modal, { memQuery: v })) }, t('scenes.mem.search')),
2844
- items.length
2845
- ? React.createElement('div', null, items.map(function (m) {
2846
- return pickRow({
2847
- disabled: busy,
2848
- key: m.id,
2849
- checked: (sections.memories || []).indexOf(String(m.id)) >= 0,
2850
- name: m.name,
2851
- // 记忆描述是自由正文的首行,可能很长:截断到 MEM_DESC_MAX,全文放 title。
2852
- // CSS 侧还有单行省略兜底(超长不再横向溢出段边框)。
2853
- desc: m.description ? clipText(m.description, MEM_DESC_MAX) : null,
2854
- descTitle: m.description || null,
2855
- onChange: function () { toggleMemory(String(m.id)) },
2856
- })
2857
- }))
2858
- : React.createElement('div', { className: 'dsm-pick-empty' }, q ? t('scenes.mem.noMatch') : t('scenes.mem.emptyScene')))
2859
- : React.createElement('div', { className: 'dsm-seg-body' }, React.createElement('div', { className: 'dsm-pick-empty' }, t('scenes.mem.hint'))),
2860
- foot: archiveSegFoot(defined, memCheckedCount()),
3541
+ count: t('scenes.seg.checked', { checked: enabledCount, total: own.length }),
3542
+ actions: segActions({
3543
+ defined: true, busy: busy,
3544
+ selectAllLabel: t('scenes.seg.selectAll'), clearLabel: t('scenes.seg.clear'),
3545
+ onAll: function () { setAllMemories(true) },
3546
+ onClear: function () { setAllMemories(false) },
3547
+ allChecked: own.length > 0 && enabledCount === own.length,
3548
+ }),
3549
+ body: React.createElement('div', { className: 'dsm-seg-body' },
3550
+ segFilter(modal.memQuery, function (v) { setModal(Object.assign({}, modal, { memQuery: v })) }, t('scenes.mem.search')),
3551
+ items.length
3552
+ ? React.createElement('div', null, items.map(function (m) {
3553
+ return pickRow({
3554
+ disabled: busy,
3555
+ key: m.id,
3556
+ checked: memoryEnabled(String(m.id)),
3557
+ name: m.name,
3558
+ // 记忆描述是自由正文的首行,可能很长:截断到 MEM_DESC_MAX,全文放 title。
3559
+ // CSS 侧还有单行省略兜底(超长不再横向溢出段边框)。
3560
+ desc: m.description ? clipText(m.description, MEM_DESC_MAX) : null,
3561
+ descTitle: m.description || null,
3562
+ onChange: function () { toggleMemory(String(m.id)) },
3563
+ })
3564
+ }))
3565
+ : React.createElement('div', { className: 'dsm-pick-empty' }, q ? t('scenes.mem.noMatch') : t('scenes.mem.emptyScene'))),
2861
3566
  })
2862
3567
  }
2863
3568
  /** 段 1:MCP 工具集(服务器级勾选 → 行内「选工具」进工具明细,明细留在同一段内)。 */
@@ -2931,15 +3636,44 @@ function callApi(path, options) {
2931
3636
  })
2932
3637
  }
2933
3638
  /** 段 2:技能集。 */
3639
+ /**
3640
+ * 技能行的人类可读展示:技能名 + 来源名。
3641
+ *
3642
+ * 勾选集里存的仍是 `key`(`<来源 key>/<技能名>`)——它才是身份的载体;但**显示**
3643
+ * 必须用技能名与来源名:自定义目录的来源 key 是 `custom-<hash>`,直接显示 key 会
3644
+ * 让导入的技能在场景里变成一串哈希(用户 2026-09-15 报的)。
3645
+ * 老宿主没回传 name/root* 时退回按 key 拆,至少不再显示整串哈希前缀之外的路径。
3646
+ */
3647
+ function skillDisplay(item) {
3648
+ var key = String((item && item.key) || '')
3649
+ var slash = key.indexOf('/')
3650
+ var keyName = slash >= 0 ? key.slice(slash + 1) : key
3651
+ var keyRoot = slash >= 0 ? key.slice(0, slash) : ''
3652
+ var root = {
3653
+ key: String((item && item.rootKey) || keyRoot),
3654
+ label: String((item && item.rootLabel) || keyRoot),
3655
+ localeKey: item && item.rootLocaleKey,
3656
+ kind: item && item.rootKind,
3657
+ }
3658
+ return {
3659
+ name: String((item && item.name) || keyName || key),
3660
+ source: root.key ? rootDisplayName(t, root) : '',
3661
+ key: key,
3662
+ }
3663
+ }
2934
3664
  function skillsSeg() {
2935
3665
  var sections = modalSections()
2936
3666
  var defined = !!sections.skills
3667
+ var allItems = modal.inventory.skills || []
2937
3668
  var items = (function () {
2938
3669
  var q = String(modal.skillQuery || '').trim().toLowerCase()
2939
- var all = modal.inventory.skills || []
2940
- return q ? all.filter(function (x) { return String(x.key).toLowerCase().indexOf(q) >= 0 }) : all
3670
+ if (!q) return allItems
3671
+ // 搜索同时匹配技能名与来源名(用户按"来源"找技能是常见动作)。
3672
+ return allItems.filter(function (x) {
3673
+ var d = skillDisplay(x)
3674
+ return (d.name + ' ' + d.source + ' ' + d.key).toLowerCase().indexOf(q) >= 0
3675
+ })
2941
3676
  })()
2942
- var allItems = modal.inventory.skills || []
2943
3677
  return seg({
2944
3678
  title: t('memory.archive.skills'),
2945
3679
  count: defined ? t('scenes.seg.checked', { checked: sections.skills.length, total: allItems.length }) : t('scenes.archive.sectionOff'),
@@ -2953,12 +3687,17 @@ function callApi(path, options) {
2953
3687
  segFilter(modal.skillQuery, function (v) { setModal(Object.assign({}, modal, { skillQuery: v })) }, t('scenes.filter.skills')),
2954
3688
  items.length
2955
3689
  ? React.createElement('div', null, items.map(function (item) {
3690
+ var d = skillDisplay(item)
2956
3691
  return pickRow({
2957
3692
  disabled: busy,
2958
3693
  key: item.key,
2959
3694
  checked: sections.skills.indexOf(item.key) >= 0,
2960
- name: item.key,
2961
- meta: item.enabled === false ? React.createElement('span', { className: 'dsm-tag dsm-tag-off' }, t('memory.scene.off')) : null,
3695
+ name: d.name,
3696
+ desc: d.source || null,
3697
+ descTitle: d.key,
3698
+ meta: item.shadowed
3699
+ ? React.createElement('span', { className: 'dsm-tag dsm-tag-off' }, t('status.shadowed'))
3700
+ : (item.enabled === false ? React.createElement('span', { className: 'dsm-tag dsm-tag-off' }, t('memory.scene.off')) : null),
2962
3701
  onChange: function () {
2963
3702
  var list = sections.skills.slice(); var i = list.indexOf(item.key)
2964
3703
  if (i >= 0) list.splice(i, 1); else list.push(item.key)
@@ -2972,6 +3711,35 @@ function callApi(path, options) {
2972
3711
  })
2973
3712
  }
2974
3713
  /** 段 3:子智能体绑定。 */
3714
+ /** 模式名(官方四个走词典,自建预设用它自己的名字,不翻译)。 */
3715
+ function scenePresetLabel(id, short) {
3716
+ var presets = (modal.inventory && modal.inventory.presets) || []
3717
+ var hit = presets.filter(function (p) { return p.id === id })[0]
3718
+ var shipped = { minimal: 1, standard: 1, ptc: 1, cordis: 1 }
3719
+ if (shipped[id] === 1 && (!hit || String(hit.trust || 'system') !== 'user')) {
3720
+ return t((short ? 'preset.short.' : 'preset.name.') + id)
3721
+ }
3722
+ return hit ? String(hit.name || id) : id
3723
+ }
3724
+ /**
3725
+ * 人设的按模式工具限制摘要(「标准 白3 · PTC 黑2」)。场景本身不引入"模式"这个
3726
+ * 维度 —— 会话跑在哪个模式是建会话时定的,场景改不了;这里只如实显示配了什么。
3727
+ */
3728
+ function personaRuleSummary(p) {
3729
+ var rules = (p && p.toolsByPreset) || {}
3730
+ var order = ((modal.inventory && modal.inventory.presets) || []).map(function (x) { return x.id })
3731
+ // 按 roster 顺序排(标准 → PTC → 极简 → 创造 → 自建),文件里的书写顺序不参与展示。
3732
+ var ids = Object.keys(rules).sort(function (a, b) {
3733
+ var ia = order.indexOf(a), ib = order.indexOf(b)
3734
+ return (ia < 0 ? 999 : ia) - (ib < 0 ? 999 : ib)
3735
+ })
3736
+ if (!ids.length) return null
3737
+ return ids.map(function (id) {
3738
+ var rule = rules[id] || {}
3739
+ var count = (rule.names || []).length
3740
+ return scenePresetLabel(id, true) + ' ' + t(rule.mode === 'deny' ? 'subagents.mode.denyOn' : 'subagents.mode.allowOn', { count: count })
3741
+ }).join(' · ')
3742
+ }
2975
3743
  function subagentsSeg() {
2976
3744
  var sections = modalSections()
2977
3745
  var defined = !!sections.subagents
@@ -3001,6 +3769,9 @@ function callApi(path, options) {
3001
3769
  checked: sections.subagents.indexOf(item.name) >= 0,
3002
3770
  name: item.name,
3003
3771
  desc: item.description || null,
3772
+ // 按模式配的工具限制摘要(没配就不显示):场景改不了会话的模式,
3773
+ // 但要让用户看得见"这条人设在某些模式下是被限工具的"。
3774
+ meta: personaRuleSummary(item),
3004
3775
  onChange: function () {
3005
3776
  var list = sections.subagents.slice(); var i = list.indexOf(item.name)
3006
3777
  if (i >= 0) list.splice(i, 1); else list.push(item.name)
@@ -3019,14 +3790,13 @@ function callApi(path, options) {
3019
3790
  */
3020
3791
  function archiveNode() {
3021
3792
  if (!modal || modal.type !== 'scene-archive') return null
3022
- return React.createElement(Modal, { key: 'sarch', wide: true, className: 'dsm-modal-archive', title: t('memory.archive.title') + ' · ' + (modal.name === '' ? t('memory.scene.global') : modal.name), closeLabel: t('btn.cancel'), onClose: function () { setModal(null) } },
3793
+ return React.createElement(Modal, { key: 'sarch', wide: true, className: 'dsm-modal-archive', title: t('memory.archive.title') + ' · ' + (modal.name === '' ? t('memory.scene.global') : modal.name), closeLabel: t('btn.close'), onClose: function () { setModal(null) } },
3023
3794
  React.createElement('div', { className: 'dsm-form' },
3024
3795
  React.createElement('div', { className: 'dsm-archive-meta' },
3025
3796
  React.createElement('span', null, t('scenes.archive.summary', {
3026
3797
  mcp: modal.sections.mcp ? Object.keys(modal.sections.mcp).length : 0,
3027
3798
  skills: modal.sections.skills ? modal.sections.skills.length : 0,
3028
3799
  subagents: modal.sections.subagents ? modal.sections.subagents.length : 0,
3029
- memories: modal.sections.memories ? modal.sections.memories.length : 0,
3030
3800
  })),
3031
3801
  React.createElement('span', { className: 'dsm-help' }, t('scenes.archive.note'))),
3032
3802
  mcpSeg(),
@@ -3056,7 +3826,9 @@ function callApi(path, options) {
3056
3826
  function submitArchive() {
3057
3827
  if (!modalSections()) return
3058
3828
  setBusy(true)
3059
- apiCall('scene-archive-save', { scene: modal.name, archive: (function () { var payload = {}; if (modal.sections.mcp) payload.mcp = modal.sections.mcp; if (modal.sections.skills) payload.skills = modal.sections.skills; if (modal.sections.subagents) payload.subagents = modal.sections.subagents; if (modal.sections.memories) payload.memories = modal.sections.memories; return payload })() }).then(function (res) {
3829
+ // P5:档案里**不再写 memories 段** —— 记忆的开关是 `rules[*].enabled` 单一真相源,
3830
+ // 在段里勾选时已经即时提交了,这里没有「待保存」的记忆状态。
3831
+ apiCall('scene-archive-save', { scene: modal.name, archive: (function () { var payload = {}; if (modal.sections.mcp) payload.mcp = modal.sections.mcp; if (modal.sections.skills) payload.skills = modal.sections.skills; if (modal.sections.subagents) payload.subagents = modal.sections.subagents; return payload })() }).then(function (res) {
3060
3832
  setBusy(false)
3061
3833
  if (res && res.ok) {
3062
3834
  setModal(null)
@@ -3074,18 +3846,31 @@ function callApi(path, options) {
3074
3846
  apiCall('rules-set-active', { scenes: [name] }).then(function (act) {
3075
3847
  setBusy(false)
3076
3848
  var staleNote = res.stale && res.stale.length ? ' · ' + t('memory.archive.stale', { items: res.stale.join('、') }) : ''
3077
- if (act && act.ok) setResult({ ok: true, text: t('memory.result.modeSet', { name: name }) + staleNote })
3078
- else setResult({ ok: false, text: t('memory.result.modeSet', { name: name }) + staleNote + ' · ' + translateError(t, act) })
3849
+ // 如实报「上层切了几台服务器 / 几个来源」——否则用户不知道勾选到底生效没有。
3850
+ var switchNote = res.switched && (res.switched.mcpServers || res.switched.skillSources)
3851
+ ? ' · ' + t('memory.result.modeSwitched', { mcp: res.switched.mcpServers, skills: res.switched.skillSources })
3852
+ : ''
3853
+ if (act && act.ok) setResult({ ok: true, text: t('memory.result.modeSet', { name: name }) + switchNote + staleNote })
3854
+ else setResult({ ok: false, text: t('memory.result.modeSet', { name: name }) + switchNote + staleNote + ' · ' + translateError(t, act) })
3079
3855
  refresh(true)
3080
3856
  }).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }); refresh(true) })
3081
3857
  }).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
3082
3858
  }
3859
+ /**
3860
+ * 退出模式(P6):与开关**同一条路径** —— 恢复档案 + 取消场景启用,两者一起写。
3861
+ * 顶部横幅的「退出模式」按钮和卡片开关点它,效果完全一致(用户裁定:「是真的退出了」)。
3862
+ */
3083
3863
  function exitMode() {
3084
3864
  setBusy(true)
3085
3865
  apiCall('scene-mode-set', { scene: null }).then(function (res) {
3086
- setBusy(false)
3087
- if (res && res.ok) { setResult({ ok: true, text: t('memory.result.modeExited') }); refresh(true) }
3088
- else setResult({ ok: false, text: translateError(t, res) })
3866
+ if (!res || !res.ok) { setBusy(false); setResult({ ok: false, text: translateError(t, res) }); return }
3867
+ // 档案已恢复 → 同步取消场景启用,保证 mode.scene 与 active 恒等。
3868
+ apiCall('rules-set-active', { scenes: [] }).then(function (act) {
3869
+ setBusy(false)
3870
+ if (act && act.ok) setResult({ ok: true, text: t('memory.result.modeExited') })
3871
+ else setResult({ ok: false, text: t('memory.result.modeExited') + ' · ' + translateError(t, act) })
3872
+ refresh(true)
3873
+ }).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }); refresh(true) })
3089
3874
  }).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
3090
3875
  }
3091
3876
  var modeScene = data.mode && data.mode.scene
@@ -3122,21 +3907,24 @@ function callApi(path, options) {
3122
3907
  if (archive.mcp) parts.push(t('scenes.profile.mcp', { count: Object.keys(archive.mcp).length }))
3123
3908
  if (Array.isArray(archive.skills)) parts.push(t('scenes.profile.skills', { count: archive.skills.length }))
3124
3909
  if (Array.isArray(archive.subagents)) parts.push(t('scenes.profile.subagents', { count: archive.subagents.length }))
3125
- if (Array.isArray(archive.memories)) parts.push(t('scenes.profile.memories', { count: archive.memories.length }))
3910
+ // P5:记忆**不再是档案的一部分**(开关是 `rules[*].enabled` 单一真相源),
3911
+ // 所以这里不再统计 memories —— 否则会显示一个已经不存在的段的数量。
3126
3912
  return parts.join(' · ')
3127
3913
  }
3914
+ // 当前模式场景被锁 → 「退出模式」按钮与开关一起禁用(服务端守卫兜底)。
3915
+ var modeSceneLocked = !!(modeScene && (data.scenes || []).some(function (s) { return s.name === modeScene && s.locked === true }))
3128
3916
  var modeSummary = modeScene ? archiveSummary(data.archives[modeScene]) : ''
3129
3917
  return React.createElement('section', { className: 'dsm-section' },
3130
3918
  React.createElement('div', { className: 'dsm-head' },
3131
3919
  React.createElement('div', { className: 'dsm-title-block' },
3132
3920
  React.createElement('div', { className: 'dsm-title-row' },
3133
- React.createElement('h2', { className: 'dsm-title' }, t('scenes.title')),
3134
- VersionBadgeComponent ? React.createElement(VersionBadgeComponent, null) : null),
3921
+ React.createElement('h2', { className: 'dsm-title' }, t('scenes.title'))),
3135
3922
  React.createElement('p', { className: 'dsm-desc' }, t('scenes.desc'))),
3136
3923
  React.createElement('div', { className: 'dsm-actions' },
3137
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || data.loading, onClick: function () { refresh() } }, t('memory.btn.refresh')),
3138
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: openCreateScene }, t('memory.btn.newScene')))),
3139
- React.createElement('div', { key: 'stats', className: 'dsm-summary dsm-summary-3' },
3924
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || data.loading, onClick: function () { refresh() } }, (busy || data.loading) ? t('btn.refreshing') : t('btn.refresh')),
3925
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: openCreateScene }, t('memory.btn.newScene')),
3926
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: function () { loadTrash(false) } }, t('trash.btn.open')))),
3927
+ React.createElement('div', { key: 'stats', className: 'dsm-summary' },
3140
3928
  [[sceneStats.total, t('scenes.stat.total')], [sceneStats.active, t('scenes.stat.active')], [sceneStats.archives, t('scenes.stat.archives')]].map(function (item) {
3141
3929
  return React.createElement('div', { key: item[1], className: 'dsm-stat' },
3142
3930
  React.createElement('strong', null, item[0]), item[1])
@@ -3152,13 +3940,13 @@ function callApi(path, options) {
3152
3940
  t('scenes.mode.active', { name: sceneLabel(data.scenes, modeScene) || modeScene })),
3153
3941
  React.createElement('div', { className: 'dsm-mode-sub' },
3154
3942
  modeSummary ? t('scenes.mode.profile', { parts: modeSummary }) : t('scenes.mode.noProfile'))),
3155
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: exitMode }, t('memory.mode.exit'))) : null,
3943
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || modeSceneLocked, title: modeSceneLocked ? t('scenes.lock.blockedExit') : '', onClick: exitMode }, t('memory.mode.exit'))) : null,
3156
3944
  React.createElement(Notice, { key: 'notice', kind: result && result.ok ? 'ok' : 'err', text: result && result.text }),
3157
3945
  // 历史「全部启用」(active=null)遗留:单选模型下多名场景同时在场是歧义状态。
3158
3946
  // 不静默改写(那会改变注入范围),而是给一个显式收敛按钮,并说清点下去会发生什么。
3159
3947
  legacyAllScenes.length > 1 ? React.createElement('div', { key: 'legacyall', className: 'dsm-feedback dsm-warning' },
3160
3948
  React.createElement('span', null, t('scenes.legacyAll', { count: legacyAllScenes.length })),
3161
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { setActiveScenes([legacyAllScenes[0].name]) } }, t('scenes.legacyAll.fix'))) : null,
3949
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { enterMode(legacyAllScenes[0].name) } }, t('scenes.legacyAll.fix'))) : null,
3162
3950
  data.error ? React.createElement('div', { key: 'gerr', className: 'dsm-feedback dsm-error' }, String(data.error)) : null,
3163
3951
  data.loading ? React.createElement('div', { className: 'dsm-empty' }, t('memory.loading'))
3164
3952
  : presetScenes.length ? React.createElement('div', { key: 'scenes', className: 'dsm-scenes' }, presetScenes.map(function (scene) {
@@ -3168,8 +3956,8 @@ function callApi(path, options) {
3168
3956
  // 全局已被上面的 presetScenes 过滤掉,这里只会遇到可切换的预设;
3169
3957
  // 历史保留场景 `_shared`(「常开」)仍然恒定注入,所以不给开关也不给删除。
3170
3958
  var locked = scene.shared === true
3959
+ var sceneLocked = scene.locked === true
3171
3960
  // 档案里有任何一段才值得「切入此模式」——空档案切进去等于什么都没变。
3172
- var hasModeSections = !!(archive && (archive.mcp || Array.isArray(archive.skills) || Array.isArray(archive.subagents) || Array.isArray(archive.memories)))
3173
3961
  var desc = sceneTileDesc(scene)
3174
3962
  // 单选:别的场景已启用时,这个开关置灰(点也没用,服务端只接受一个)。
3175
3963
  var blockedByOther = scene.active !== true && scene.selectable === false
@@ -3184,6 +3972,7 @@ function callApi(path, options) {
3184
3972
  name === label ? null : React.createElement('span', { className: 'dsm-scene-tile-key' }, name),
3185
3973
  locked ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('memory.scene.shared')) : null,
3186
3974
  modeScene === name ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('memory.mode.current')) : null,
3975
+ sceneLocked ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('scenes.lock.tag')) : null,
3187
3976
  !locked && scene.active === false ? React.createElement('span', { className: 'dsm-tag dsm-tag-off' }, t('memory.scene.off')) : null,
3188
3977
  boundPrompt ? React.createElement('span', {
3189
3978
  className: 'dsm-tag' + (promptMissing ? ' dsm-tag-off' : promptLive ? ' dsm-tag-on' : ''),
@@ -3193,25 +3982,25 @@ function callApi(path, options) {
3193
3982
  : promptLive ? t('scenes.prompt.live', { id: boundPrompt }) : t('scenes.prompt.bound', { id: boundPrompt })) : null,
3194
3983
  locked ? null : React.createElement('span', {
3195
3984
  className: 'dsm-scene-tile-switch',
3196
- title: blockedByOther ? t('scenes.enable.blocked', { name: sceneLabel(data.scenes, data.activeScene) || data.activeScene || '' }) : '',
3197
- }, React.createElement(Switch, { on: scene.active === true, disabled: busy || blockedByOther, label: t('memory.scene.enable') + ' ' + name, onClick: function () { toggleScene(scene) } }))),
3985
+ title: blockedByOther ? t('scenes.enable.blocked', { name: sceneLabel(data.scenes, data.activeScene) || data.activeScene || '' }) : (sceneLocked && scene.active === true ? t('scenes.lock.blockedExit') : ''),
3986
+ }, React.createElement(Switch, { on: scene.active === true, disabled: busy || blockedByOther || (sceneLocked && scene.active === true), label: t('memory.scene.enable') + ' ' + name, onClick: function () { toggleScene(scene) } })),
3987
+ locked ? null : React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-lock', disabled: busy || scene.active !== true, title: scene.active !== true ? t('scenes.lock.notActive') : t('scenes.lock.hint'), onClick: function () { toggleSceneLock(scene) } }, sceneLocked ? t('scenes.lock.unlock') : t('scenes.lock.lock'))),
3198
3988
  // 描述行**只有描述**(数量都收进上面的模式条了);全文放 title,卡片本身永远一行。
3199
3989
  React.createElement('p', { className: 'dsm-scene-tile-desc', title: desc || '' }, desc || t('scenes.noDesc')),
3990
+ // P6:「切入此模式」按钮已删除 —— 右上角开关就是唯一入口(开 = 进入,关 = 回全局默认)。
3200
3991
  React.createElement('div', { className: 'dsm-scene-tile-foot' },
3201
- hasModeSections && modeScene !== name ? React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: function () { enterMode(name) } }, t('scenes.mode.enter')) : null,
3202
3992
  React.createElement('div', { className: 'dsm-scene-tile-links' },
3203
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { openArchive(name) } }, t('memory.archive.edit')),
3993
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || sceneLocked, title: sceneLocked ? t('scenes.lock.blockedEdit') : '', onClick: function () { openArchive(name) } }, t('memory.archive.edit')),
3204
3994
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { openEditScene(scene) } }, t('memory.scene.edit')),
3205
- locked ? null : React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy, onClick: function () { setModal({ type: 'scene-delete', name: name }) } }, t('memory.btn.deleteScene')))))
3995
+ locked ? null : React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy || sceneLocked, title: sceneLocked ? t('scenes.lock.blockedEdit') : '', onClick: function () { setModal({ type: 'scene-delete', name: name }) } }, t('memory.btn.deleteScene')))))
3206
3996
  })) : React.createElement('div', { key: 'empty', className: 'dsm-empty' }, t('scenes.empty')),
3207
- modal && (modal.type === 'scene-create' || modal.type === 'scene-edit') ? React.createElement(Modal, { key: 'screate', title: modal.type === 'scene-create' ? t('memory.scene.createTitle') : t('memory.scene.editTitle'), closeLabel: t('btn.cancel'), onClose: function () { setModal(null) } },
3997
+ modal && (modal.type === 'scene-create' || modal.type === 'scene-edit') ? React.createElement(Modal, { key: 'screate', title: modal.type === 'scene-create' ? t('memory.scene.createTitle') : t('memory.scene.editTitle'), closeLabel: t('btn.close'), onClose: function () { setModal(null) } },
3208
3998
  React.createElement('div', { className: 'dsm-form' },
3209
3999
  React.createElement('label', { className: 'dsm-field' },
3210
4000
  React.createElement('span', { className: 'dsm-label' }, t('memory.scene.field.name')),
3211
4001
  React.createElement('input', {
3212
4002
  className: 'dsm-control' + (sceneForm.error ? ' dsm-rule-invalid' : ''),
3213
4003
  value: sceneForm.name || '',
3214
- disabled: modal.type === 'scene-edit',
3215
4004
  placeholder: t('memory.scene.field.name.placeholder'),
3216
4005
  onChange: function (e) { setSceneForm(Object.assign({}, sceneForm, { name: e.target.value, error: null })) },
3217
4006
  }),
@@ -3238,12 +4027,25 @@ function callApi(path, options) {
3238
4027
  React.createElement('p', { className: 'dsm-help' }, t('scenes.field.prompt.hint'))),
3239
4028
  React.createElement('div', { className: 'dsm-modal-actions' },
3240
4029
  React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy || !String(sceneForm.name || '').trim(), onClick: submitSceneForm }, t(modal.type === 'scene-create' ? 'memory.btn.create' : 'memory.btn.saveScene'))))) : null,
3241
- modal && modal.type === 'scene-delete' ? React.createElement(Modal, { key: 'sdel', title: t('memory.deleteScene.title'), closeLabel: t('btn.cancel'), onClose: function () { setModal(null) } },
4030
+ modal && modal.type === 'scene-delete' ? React.createElement(Modal, { key: 'sdel', title: t('memory.deleteScene.title'), closeLabel: t('btn.close'), onClose: function () { setModal(null) } },
3242
4031
  React.createElement('p', { className: 'dsm-help' }, t('memory.deleteScene.desc', { name: modal.name })),
3243
4032
  React.createElement('div', { className: 'dsm-modal-actions' },
3244
4033
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { submitDeleteScene(modal.name) } }, t('memory.btn.deleteScene')))) : null,
3245
4034
  // 档案编辑器(见 archiveNode):三段共用「段卡片 + 勾选行」排版。
3246
4035
  archiveNode(),
4036
+ trash ? React.createElement(TrashModal, {
4037
+ t: t,
4038
+ title: t('trash.title'),
4039
+ groupTitle: t('trash.group.scenes'),
4040
+ groupSub: t('trash.section.scenes.sub'),
4041
+ entries: trash.entries,
4042
+ loading: trash.loading,
4043
+ error: trash.error,
4044
+ busy: trashBusy,
4045
+ onClose: function () { setTrash(null) },
4046
+ onRestore: restoreScene,
4047
+ onPurge: purgeScene,
4048
+ }) : null,
3247
4049
  )
3248
4050
  }
3249
4051
  // ---------- 子智能体页:人设文件(~/.dsh/subagents/*.md)管理 ----------
@@ -3256,20 +4058,63 @@ function callApi(path, options) {
3256
4058
  var result = rs[0], setResult = rs[1]
3257
4059
  var ms = React.useState(null)
3258
4060
  var modal = ms[0], setModal = ms[1]
4061
+ // 场景锁定(v0.8):任一场景锁定 = 五个域整体冻结;本页全部写控件禁用。
4062
+ var anyLocked = data.anyLocked === true
4063
+ // 回收站:删除人设 = 移入回收站(宿主侧 subagent-delete),这里列出/恢复/永久删除。
4064
+ var ts = React.useState(null)
4065
+ var trash = ts[0], setTrash = ts[1]
4066
+ var tb = React.useState(false)
4067
+ var trashBusy = tb[0], setTrashBusy = tb[1]
4068
+ function loadTrash(keepOpen) {
4069
+ setTrash(Object.assign({ loading: true, error: null, entries: [] }, keepOpen ? trash || {} : {}))
4070
+ apiCall('subagent-trash-list', {}).then(function (res) {
4071
+ if (res && res.ok) setTrash({ loading: false, error: null, entries: res.trash || [] })
4072
+ else setTrash({ loading: false, error: (res && res.error) || t('trash.loadFailed'), entries: [] })
4073
+ }).catch(function (e) { setTrash({ loading: false, error: String((e && e.message) || e), entries: [] }) })
4074
+ }
4075
+ function restorePersona(item) {
4076
+ setTrashBusy(true)
4077
+ apiCall('subagent-trash-restore', { id: item.id }).then(function (res) {
4078
+ setTrashBusy(false)
4079
+ if (res && res.ok) { setResult({ ok: true, text: t('trash.restored', { name: item.name }) }); refresh(true); loadTrash(true) }
4080
+ else setTrash(function (cur) { return Object.assign({}, cur, { error: (res && res.error) || t('mcp.msg.failed') }) })
4081
+ }).catch(function (e) { setTrashBusy(false); setTrash(function (cur) { return Object.assign({}, cur, { error: String((e && e.message) || e) }) }) })
4082
+ }
4083
+ function purgePersona(item) {
4084
+ setTrashBusy(true)
4085
+ apiCall('subagent-trash-delete', { id: item.id }).then(function (res) {
4086
+ setTrashBusy(false)
4087
+ if (res && res.ok) { setResult({ ok: true, text: t('trash.purged', { name: item.name }) }); loadTrash(true) }
4088
+ else setTrash(function (cur) { return Object.assign({}, cur, { error: (res && res.error) || t('mcp.msg.failed') }) })
4089
+ }).catch(function (e) { setTrashBusy(false); setTrash(function (cur) { return Object.assign({}, cur, { error: String((e && e.message) || e) }) }) })
4090
+ }
3259
4091
  React.useEffect(function () { if (!result || result.ok !== true) return undefined; var timer = setTimeout(function () { setResult(null) }, 2600); return function () { clearTimeout(timer) } }, [result])
3260
4092
  function refresh(silent) {
3261
4093
  if (!silent) setData(function (prev) { return Object.assign({}, prev, { loading: true, error: null }) })
3262
4094
  apiCall('subagent-list', {}).then(function (r) {
3263
- if (r && r.ok) setData({ loading: false, error: null, subagents: r.subagents || [] })
4095
+ if (r && r.ok) setData({ loading: false, error: null, subagents: r.subagents || [], anyLocked: r.anyLocked === true })
3264
4096
  else setData({ loading: false, error: translateError(t, r), subagents: [] })
3265
4097
  }).catch(function (e) { setData({ loading: false, error: String((e && e.message) || e), subagents: [] }) })
3266
4098
  }
3267
4099
  React.useEffect(function () { refresh() }, [])
4100
+ /**
4101
+ * 子智能体开关(与记忆页同一套交互):停用 = 不注入目录段、subagent_list/run
4102
+ * 不可见;人设文件不动。开关联动全局状态,立即提交。
4103
+ */
4104
+ function togglePersona(p) {
4105
+ var next = !(p.enabled !== false)
4106
+ setBusy(true)
4107
+ apiCall('subagent-toggle', { name: p.name, enabled: next }).then(function (r) {
4108
+ setBusy(false)
4109
+ if (r && r.ok) refresh(true)
4110
+ else if (r) setResult({ ok: false, text: translateError(t, r) })
4111
+ }).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
4112
+ }
3268
4113
  /**
3269
4114
  * 人设表单的候选数据(模型目录 / 全体预设工具并集)只在**首次展开高级选项**时拉取:
3270
4115
  * 宿主枚举预设需要为尚未挂载的预设建立 standing mount,不该在打开弹窗时就付这个代价。
3271
4116
  */
3272
- var cands = React.useState({ loaded: false, loading: false, error: null, models: [], tools: [] })
4117
+ var cands = React.useState({ loaded: false, loading: false, error: null, models: [], tools: [], presets: [] })
3273
4118
  var cand = cands[0], setCand = cands[1]
3274
4119
  function loadCandidates() {
3275
4120
  if (cand.loaded || cand.loading) return
@@ -3282,18 +4127,31 @@ function callApi(path, options) {
3282
4127
  error: failed ? t('subagents.adv.loadFailed') : null,
3283
4128
  models: (mres && mres.models) || [],
3284
4129
  tools: (tres && tres.tools) || [],
4130
+ // presets 必须原样带过来:四行模式的名字、顺序、每行的工具清单全靠它,
4131
+ // 早先这里只留 models/tools,于是分组标题退回裸 id(中文界面里显示英文)。
4132
+ presets: (tres && tres.presets) || [],
3285
4133
  })
3286
4134
  }).catch(function (e) {
3287
- setCand({ loaded: true, loading: false, error: String((e && e.message) || e), models: [], tools: [] })
4135
+ setCand({ loaded: true, loading: false, error: String((e && e.message) || e), models: [], tools: [], presets: [] })
3288
4136
  })
3289
4137
  }
3290
4138
  /** 高级选项:默认收起;已经在用模型/工具限制的人设自动展开(否则用户看不见自己配了什么)。 */
3291
4139
  function initialAdvanced(p) {
3292
- return !!((p && (p.model || p.provider)) || (p && ((p.tools || []).length || (p.toolsDeny || []).length)))
4140
+ var modes = p && p.toolsByPreset ? Object.keys(p.toolsByPreset).length : 0
4141
+ return !!((p && (p.model || p.provider)) || (p && ((p.tools || []).length || (p.toolsDeny || []).length || modes)))
4142
+ }
4143
+ /** 编辑态里按模式分组的名单(深拷贝:取消编辑不留痕)。 */
4144
+ function cloneRules(source) {
4145
+ var out = {}
4146
+ Object.keys(source || {}).forEach(function (id) {
4147
+ var rule = source[id] || {}
4148
+ out[id] = { mode: rule.mode === 'deny' ? 'deny' : 'allow', names: (rule.names || []).slice() }
4149
+ })
4150
+ return out
3293
4151
  }
3294
4152
  function openEditor(name) {
3295
4153
  if (!name) {
3296
- setModal({ type: 'editor', mode: 'create', advanced: false, form: { name: '', description: '', provider: '', model: '', tools: [], toolsDeny: [], body: '', error: null } })
4154
+ setModal({ type: 'editor', mode: 'create', advanced: false, openMode: null, modeQuery: '', legacyTarget: '', form: { name: '', description: '', provider: '', model: '', tools: [], toolsDeny: [], toolsByPreset: {}, body: '', error: null } })
3297
4155
  return
3298
4156
  }
3299
4157
  setBusy(true)
@@ -3301,9 +4159,10 @@ function callApi(path, options) {
3301
4159
  setBusy(false)
3302
4160
  if (res && res.ok) {
3303
4161
  var p = res.persona || {}
3304
- setModal({ type: 'editor', mode: 'edit', advanced: initialAdvanced(p), form: {
4162
+ setModal({ type: 'editor', mode: 'edit', originalName: String(p.name || name), advanced: initialAdvanced(p), openMode: null, modeQuery: '', legacyTarget: '', form: {
3305
4163
  name: p.name || name, description: p.description || '', provider: p.provider || '', model: p.model || '',
3306
- tools: (p.tools || []).slice(), toolsDeny: (p.toolsDeny || []).slice(), body: p.body || '', error: null,
4164
+ tools: (p.tools || []).slice(), toolsDeny: (p.toolsDeny || []).slice(),
4165
+ toolsByPreset: cloneRules(p.toolsByPreset), body: p.body || '', error: null,
3307
4166
  } })
3308
4167
  } else setResult({ ok: false, text: translateError(t, res) })
3309
4168
  }).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) })
@@ -3329,72 +4188,176 @@ function callApi(path, options) {
3329
4188
  return hit ? (f.provider + '\u0000' + f.model) : '__custom__'
3330
4189
  }
3331
4190
  /**
3332
- * 工具选择器(白名单 / 黑名单共用)。
3333
- *
3334
- * 候选 = 宿主回传的**全体 Agent 预设工具并集**,并标注「当前会话可见」:
3335
- * 人设可能在任何预设下被子代理复用,只列当前会话的工具会让换预设后的子代理启动失败
3336
- * (官方 toolFilter 对未知名直接拒绝启动)。因此这里宁可多列,并把手填入口留出来。
4191
+ * 官方四个预设的中英名走插件词典(官方 preset.yml 里只有中文名,直接用会让英文界面
4192
+ * 露出中文);**自建预设用它自己的名字,不翻译** —— 与官方 display 规则一致。
4193
+ * 四个预设 id → 词典键的映射只认官方发布的这四个 id。
3337
4194
  */
3338
- function toolPicker(props) {
3339
- var all = cand.tools || []
3340
- var q = String(modal.toolQuery || '').trim().toLowerCase()
3341
- var list = q ? all.filter(function (x) { return String(x.name).toLowerCase().indexOf(q) >= 0 }) : all
3342
- var picked = props.selected || []
3343
- // 不再把官方预设之外的工具统一标成「当前会话不可见」。工具白/黑名单是
3344
- // 子代理启动时按 Agent 预设装配的:不同官方预设拥有不同的工具集合,
3345
- // 选择器必须按预设展示来源,用户才能知道这个工具在哪个预设下可用。
3346
- var presetMap = {}
3347
- ;(cand.presets || []).forEach(function (p) { presetMap[p.id] = p.name || p.id })
3348
- var grouped = {}
3349
- list.forEach(function (item) {
3350
- var ids = Array.isArray(item.presets) && item.presets.length ? item.presets : ['__other__']
3351
- ids.forEach(function (id) {
3352
- if (!grouped[id]) grouped[id] = []
3353
- grouped[id].push(item)
3354
- })
3355
- })
3356
- var groupIds = Object.keys(grouped).sort(function (a, b) {
3357
- if (a === '__other__') return 1
3358
- if (b === '__other__') return -1
3359
- return String(presetMap[a] || a).localeCompare(String(presetMap[b] || b))
3360
- })
3361
- function row(item, id) {
3362
- return pickRow({
3363
- disabled: busy,
3364
- key: (props.deny ? 'd:' : 'a:') + id + ':' + item.name,
3365
- checked: picked.indexOf(item.name) >= 0,
3366
- name: item.name,
3367
- onChange: function () { props.onToggle(item.name) },
3368
- })
4195
+ var SHIPPED_PRESET_IDS = { minimal: 1, standard: 1, ptc: 1, cordis: 1 }
4196
+ function presetLabel(p) {
4197
+ if (!p) return ''
4198
+ var id = String(p.id || '')
4199
+ if (SHIPPED_PRESET_IDS[id] === 1 && String(p.trust || 'system') !== 'user') return t('preset.name.' + id)
4200
+ return String(p.name || id)
4201
+ }
4202
+ /** 该模式下可勾选的工具名;`null` = 宿主没给出该模式的清单(读不到,不能假装是空)。 */
4203
+ function modeTools(id) {
4204
+ var hit = (cand.presets || []).filter(function (p) { return p.id === id })[0]
4205
+ return hit && Array.isArray(hit.tools) ? hit.tools : null
4206
+ }
4207
+ function modeRule(id) { return (modal.form.toolsByPreset || {})[id] }
4208
+ function setModeRule(id, rule) {
4209
+ var next = Object.assign({}, modal.form.toolsByPreset || {})
4210
+ if (rule) next[id] = rule; else delete next[id]
4211
+ setModal(Object.assign({}, modal, {
4212
+ form: Object.assign({}, modal.form, { toolsByPreset: next }),
4213
+ openMode: modal.openMode === id && !rule ? null : modal.openMode,
4214
+ }))
4215
+ }
4216
+ /** 模式行上的一句话状态:未开启 / 白名单 N 个 / 黑名单 N 个 / 已开启但没勾(= 不限制)。 */
4217
+ function modeSummary(id) {
4218
+ var rule = modeRule(id)
4219
+ if (!rule) return { text: t('subagents.mode.off'), cls: 'dsm-mode-off' }
4220
+ var n = (rule.names || []).length
4221
+ if (!n) return { text: t('subagents.mode.empty'), cls: 'dsm-mode-off' }
4222
+ return {
4223
+ text: t(rule.mode === 'deny' ? 'subagents.mode.denyOn' : 'subagents.mode.allowOn', { count: n }),
4224
+ cls: rule.mode === 'deny' ? 'dsm-mode-deny' : 'dsm-mode-allow',
3369
4225
  }
3370
- return React.createElement('div', { className: 'dsm-field' },
3371
- React.createElement('span', { className: 'dsm-label' },
3372
- props.label + (picked.length ? ' · ' + t('subagents.tools.count', { count: picked.length }) : '')),
4226
+ }
4227
+ /**
4228
+ * 启动某一侧名单:生效 + 就地展开。同一模式内白/黑互斥 —— 启动一侧就把模式切过去,
4229
+ * **已勾选的名字保留**(只换语义),这样"改成黑名单"不用从头再勾一遍。
4230
+ */
4231
+ function startMode(id, mode) {
4232
+ var prev = modeRule(id)
4233
+ var names = prev && Array.isArray(prev.names) ? prev.names.slice() : []
4234
+ setModal(Object.assign({}, modal, {
4235
+ openMode: id, modeQuery: '',
4236
+ form: Object.assign({}, modal.form, {
4237
+ toolsByPreset: Object.assign({}, modal.form.toolsByPreset || {}, { [id]: { mode: mode, names: names } }),
4238
+ }),
4239
+ }))
4240
+ }
4241
+ function stopMode(id) { setModeRule(id, null) }
4242
+ function toggleModeTool(id, name) {
4243
+ var rule = modeRule(id)
4244
+ if (!rule) return
4245
+ setModeRule(id, { mode: rule.mode, names: toggled(rule.names || [], name) })
4246
+ }
4247
+ /**
4248
+ * 展开后的勾选区:只列**该模式自己的**工具。这样勾出来的名字天然都属于这个预设,
4249
+ * 不会出现"在标准模式勾了只属于 PTC 的工具 → 换模式跑就启动失败"。
4250
+ * MCP 工具不在这里(用户裁定):子代理照旧能用当前在跑的 MCP,那份名单在运行时并入。
4251
+ */
4252
+ function modeEditor(p) {
4253
+ var id = p.id
4254
+ var rule = modeRule(id)
4255
+ if (!rule) return null
4256
+ var all = modeTools(id)
4257
+ if (all === null || all.length === 0) {
4258
+ return React.createElement('div', { className: 'dsm-mode-body' },
4259
+ React.createElement('div', { className: 'dsm-pick-empty' }, p.broken ? t('subagents.mode.broken') : t('subagents.mode.noTools')))
4260
+ }
4261
+ var q = String(modal.modeQuery || '').trim().toLowerCase()
4262
+ var list = q ? all.filter(function (n) { return n.toLowerCase().indexOf(q) >= 0 }) : all
4263
+ var picked = rule.names || []
4264
+ return React.createElement('div', { className: 'dsm-mode-body' },
3373
4265
  picked.length
3374
4266
  ? React.createElement('div', { className: 'dsm-tools-chips' }, picked.map(function (name) {
3375
- return React.createElement('span', { key: 'c:' + name, className: 'dsm-chip' + (props.deny ? ' dsm-chip-deny' : '') },
4267
+ return React.createElement('span', { key: 'c:' + name, className: 'dsm-chip' + (rule.mode === 'deny' ? ' dsm-chip-deny' : '') },
3376
4268
  name,
3377
- React.createElement('button', { type: 'button', title: t('subagents.tools.remove'), onClick: function () { props.onToggle(name) } }, '×'))
4269
+ React.createElement('button', { type: 'button', title: t('subagents.tools.remove'), onClick: function () { toggleModeTool(id, name) } }, '×'))
3378
4270
  }))
3379
- : React.createElement('span', { className: 'dsm-adv-note' }, props.chosen),
4271
+ : React.createElement('span', { className: 'dsm-adv-note' }, t(rule.mode === 'deny' ? 'subagents.mode.denyEmpty' : 'subagents.mode.allowEmpty')),
3380
4272
  React.createElement('div', { className: 'dsm-combo-row' },
3381
4273
  React.createElement('input', {
3382
4274
  className: 'dsm-control',
3383
- value: modal.toolQuery || '',
3384
- placeholder: props.filterPlaceholder,
3385
- onChange: function (e) { setModal(Object.assign({}, modal, { toolQuery: e.target.value })) },
4275
+ value: modal.modeQuery || '',
4276
+ placeholder: t('subagents.tools.filter'),
4277
+ onChange: function (e) { setModal(Object.assign({}, modal, { modeQuery: e.target.value })) },
3386
4278
  }),
3387
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || !all.length, onClick: props.onAll }, t('scenes.seg.selectAll')),
3388
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || !picked.length, onClick: props.onClear }, t('scenes.seg.clear'))),
3389
- React.createElement('div', { className: 'dsm-tools-grid' },
3390
- groupIds.length
3391
- ? groupIds.map(function (id) {
3392
- return React.createElement(React.Fragment, { key: 'g:' + id },
3393
- segGroup(id === '__other__' ? props.groupOther : (presetMap[id] || id)),
3394
- grouped[id].map(function (item) { return row(item, id) }))
3395
- })
3396
- : React.createElement('div', { className: 'dsm-pick-empty' }, all.length ? t('scenes.mem.noMatch') : props.empty)),
3397
- React.createElement('p', { className: 'dsm-help' }, props.hint))
4279
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { setModeRule(id, { mode: rule.mode, names: all.slice() }) } }, t('scenes.seg.selectAll')),
4280
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || !picked.length, onClick: function () { setModeRule(id, { mode: rule.mode, names: [] }) } }, t('scenes.seg.clear'))),
4281
+ list.length
4282
+ ? React.createElement('div', { className: 'dsm-tools-grid' }, list.map(function (name) {
4283
+ return pickRow({ key: 'm:' + id + ':' + name, disabled: busy, checked: picked.indexOf(name) >= 0, name: name, onChange: function () { toggleModeTool(id, name) } })
4284
+ }))
4285
+ : React.createElement('div', { className: 'dsm-pick-empty' }, t('scenes.mem.noMatch')))
4286
+ }
4287
+ /**
4288
+ * 一行一个 Agent 预设(roster 顺序:标准 / PTC / 极简 / 创造 / 自建预设)。
4289
+ * 默认全部折叠、全部未启动;右侧两个按钮「启动 白名单」「启动 黑名单」互斥。
4290
+ */
4291
+ function modeRows() {
4292
+ var presets = (cand.presets || []).slice()
4293
+ if (!presets.length) {
4294
+ return React.createElement('div', { className: 'dsm-field' },
4295
+ React.createElement('span', { className: 'dsm-label' }, t('subagents.field.modes')),
4296
+ React.createElement('div', { className: 'dsm-pick-empty' }, cand.loading ? t('memory.loading') : t('subagents.mode.noPresets')))
4297
+ }
4298
+ return React.createElement('div', { className: 'dsm-field' },
4299
+ React.createElement('span', { className: 'dsm-label' }, t('subagents.field.modes')),
4300
+ React.createElement('div', { className: 'dsm-modes' }, presets.map(function (p) {
4301
+ var id = p.id
4302
+ var rule = modeRule(id)
4303
+ var summary = modeSummary(id)
4304
+ var startButton = function (mode, isDeny) {
4305
+ var active = !!rule && rule.mode === mode
4306
+ return React.createElement('button', {
4307
+ key: mode,
4308
+ type: 'button',
4309
+ className: 'dsm-btn dsm-btn-quiet' + (active ? (mode === 'deny' ? ' dsm-mode-btn-deny' : ' dsm-mode-btn-allow') : ''),
4310
+ disabled: busy,
4311
+ onClick: function () { if (active) stopMode(id); else startMode(id, mode) },
4312
+ }, t(active
4313
+ ? (isDeny ? 'subagents.mode.stopDeny' : 'subagents.mode.stopAllow')
4314
+ : (isDeny ? 'subagents.mode.startDeny' : 'subagents.mode.startAllow')))
4315
+ }
4316
+ return React.createElement('div', { className: 'dsm-mode-row' + (rule ? ' dsm-mode-on' : ''), key: id },
4317
+ React.createElement('div', { className: 'dsm-mode-head' },
4318
+ React.createElement('span', { className: 'dsm-mode-name' }, presetLabel(p)),
4319
+ p.broken ? React.createElement('span', { className: 'dsm-tag dsm-tag-off' }, t('compat.reach.broken')) : null,
4320
+ React.createElement('span', { className: 'dsm-mode-sum ' + summary.cls }, summary.text),
4321
+ React.createElement('span', { className: 'dsm-mode-actions' },
4322
+ startButton('allow', false),
4323
+ startButton('deny', true))),
4324
+ modal.openMode === id ? modeEditor(p) : null)
4325
+ })),
4326
+ React.createElement('p', { className: 'dsm-help' }, t('subagents.field.modes.hint')))
4327
+ }
4328
+ /**
4329
+ * 旧格式(全局 `tools:` / `toolsDeny:`)的只读小结 + 转换入口。
4330
+ * 旧键对所有模式生效,运行时仍然照旧执行;**不点转换就不动文件里的旧键**。
4331
+ */
4332
+ function legacyNotice() {
4333
+ var allow = modal.form.tools || []
4334
+ var deny = modal.form.toolsDeny || []
4335
+ if (!allow.length && !deny.length) return null
4336
+ var presets = cand.presets || []
4337
+ var target = modal.legacyTarget || (presets[0] ? presets[0].id : '')
4338
+ return React.createElement('div', { className: 'dsm-legacy' },
4339
+ React.createElement('span', { className: 'dsm-adv-note' }, t('subagents.legacy.note', { allow: allow.length, deny: deny.length })),
4340
+ React.createElement('div', { className: 'dsm-combo-row' },
4341
+ React.createElement('div', { className: 'dsm-select' },
4342
+ React.createElement('select', {
4343
+ className: 'dsm-control',
4344
+ value: target,
4345
+ disabled: busy || !presets.length,
4346
+ onChange: function (e) { setModal(Object.assign({}, modal, { legacyTarget: e.target.value })) },
4347
+ }, presets.map(function (p) { return React.createElement('option', { key: p.id, value: p.id }, presetLabel(p)) }))),
4348
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || !target || !presets.length, onClick: function () {
4349
+ var rule = { mode: allow.length ? 'allow' : 'deny', names: (allow.length ? allow : deny).slice() }
4350
+ setModal(Object.assign({}, modal, {
4351
+ openMode: target,
4352
+ modeQuery: '',
4353
+ legacyTarget: target,
4354
+ form: Object.assign({}, modal.form, {
4355
+ tools: [], toolsDeny: [],
4356
+ toolsByPreset: Object.assign({}, modal.form.toolsByPreset || {}, { [target]: rule }),
4357
+ }),
4358
+ }))
4359
+ } }, t('subagents.legacy.convert'))),
4360
+ React.createElement('p', { className: 'dsm-help' }, t('subagents.legacy.hint')))
3398
4361
  }
3399
4362
  function toggleAdvanced() {
3400
4363
  if (!modal || modal.type !== 'editor') return
@@ -3406,10 +4369,16 @@ function callApi(path, options) {
3406
4369
  if (!modal || modal.type !== 'editor') return
3407
4370
  setBusy(true)
3408
4371
  var op = modal.mode === 'create' ? 'subagent-create' : 'subagent-update'
4372
+ // 编辑时名字可改:nextName 只在真的改过时才传(服务端按原名定位文件)。
4373
+ var renamed = modal.mode === 'edit' && modal.originalName && modal.originalName !== modal.form.name
3409
4374
  apiCall(op, {
3410
- name: modal.form.name, description: modal.form.description,
4375
+ name: renamed ? String(modal.originalName) : modal.form.name,
4376
+ ...(renamed ? { nextName: String(modal.form.name || '').trim() } : {}),
4377
+ description: modal.form.description,
3411
4378
  provider: modal.form.provider, model: modal.form.model,
4379
+ // 旧格式的全局名单原样回写(旧键不点转换就不动),新模式名单另存一块。
3412
4380
  tools: modal.form.tools || [], toolsDeny: modal.form.toolsDeny || [],
4381
+ toolsByPreset: modal.form.toolsByPreset || {},
3413
4382
  body: modal.form.body,
3414
4383
  }).then(function (res) {
3415
4384
  setBusy(false)
@@ -3444,13 +4413,21 @@ function callApi(path, options) {
3444
4413
  React.createElement('div', { className: 'dsm-head' },
3445
4414
  React.createElement('div', { className: 'dsm-title-block' },
3446
4415
  React.createElement('div', { className: 'dsm-title-row' },
3447
- React.createElement('h2', { className: 'dsm-title' }, t('subagents.title')),
3448
- VersionBadgeComponent ? React.createElement(VersionBadgeComponent, null) : null),
4416
+ React.createElement('h2', { className: 'dsm-title' }, t('subagents.title'))),
3449
4417
  React.createElement('p', { className: 'dsm-desc' }, t('subagents.desc'))),
3450
4418
  React.createElement('div', { className: 'dsm-actions' },
3451
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || data.loading, onClick: function () { refresh() } }, t('memory.btn.refresh')),
3452
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: function () { setResult(null); setModal({ type: 'import' }) } }, t('subagents.import')),
3453
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: function () { openEditor(null) } }, t('subagents.new')))),
4419
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || data.loading, onClick: function () { refresh() } }, (busy || data.loading) ? t('btn.refreshing') : t('btn.refresh')),
4420
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || anyLocked, onClick: function () { openEditor(null) } }, t('subagents.new')),
4421
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || anyLocked, onClick: function () { setResult(null); setModal({ type: 'import' }) } }, t('subagents.import')),
4422
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || !data.subagents.length, onClick: function () { setResult(null); setModal({ type: 'export' }) } }, t('export.subagents')),
4423
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: function () { loadTrash(false) } }, t('trash.btn.open')))),
4424
+ anyLocked === true ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'status' }, t('lock.banner')) : null,
4425
+ React.createElement('div', { className: 'dsm-summary', style: { '--dsm-stat-cols': '2' } },
4426
+ [['total', data.subagents.length, t('subagents.stat.total')],
4427
+ ['limited', data.subagents.filter(function (p) { return (p.tools && p.tools.length) || (p.toolsDeny && p.toolsDeny.length) || (p.toolsByPreset && Object.keys(p.toolsByPreset).length) }).length, t('subagents.stat.limited')]].map(function (item) {
4428
+ return React.createElement('div', { key: item[0], className: 'dsm-stat' },
4429
+ React.createElement('strong', null, item[1]), item[2])
4430
+ })),
3454
4431
  React.createElement(Notice, { kind: result && result.ok ? 'ok' : 'err', text: result && result.text }),
3455
4432
  data.error ? React.createElement('div', { className: 'dsm-feedback dsm-error' }, String(data.error)) : null,
3456
4433
  data.loading ? React.createElement('div', { className: 'dsm-empty' }, t('memory.loading'))
@@ -3458,17 +4435,19 @@ function callApi(path, options) {
3458
4435
  return React.createElement('div', { key: p.name, className: 'dsm-source' },
3459
4436
  React.createElement('div', { className: 'dsm-source-head' },
3460
4437
  React.createElement('div', { className: 'dsm-source-head-main' },
3461
- React.createElement('span', { className: 'dsm-source-title' }, p.name),
3462
- React.createElement('span', { className: 'dsm-note' }, p.description || '')),
4438
+ React.createElement('span', { className: 'dsm-source-title', title: p.name }, p.name),
4439
+ React.createElement('span', { className: 'dsm-note' }, p.description || ''),
4440
+ p.enabled === false ? React.createElement('span', { className: 'dsm-tag', title: t('subagents.disabled.hint') }, t('subagents.disabled')) : null),
3463
4441
  React.createElement('div', { className: 'dsm-source-actions' },
3464
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { openEditor(p.name) } }, t('memory.edit')),
3465
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy, onClick: function () { setModal({ type: 'delete', name: p.name }) } }, t('memory.delete')))))
4442
+ React.createElement(Switch, { on: p.enabled !== false, disabled: busy || anyLocked, label: t('subagents.toggle') + ' ' + p.name, onClick: function () { togglePersona(p) } }),
4443
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || anyLocked, onClick: function () { openEditor(p.name) } }, t('memory.edit')),
4444
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy || anyLocked, onClick: function () { setModal({ type: 'delete', name: p.name }) } }, t('memory.delete')))))
3466
4445
  })) : React.createElement('div', { className: 'dsm-empty' }, t('subagents.empty')),
3467
- modal && modal.type === 'editor' ? React.createElement(Modal, { key: 'sedit', wide: true, title: modal.mode === 'create' ? t('subagents.create') : t('subagents.edit') + ' · ' + modal.form.name, closeLabel: t('btn.cancel'), onClose: function () { setModal(null) } },
4446
+ modal && modal.type === 'editor' ? React.createElement(Modal, { key: 'sedit', wide: true, title: modal.mode === 'create' ? t('subagents.create') : t('subagents.edit') + ' · ' + modal.form.name, closeLabel: t('btn.close'), onClose: function () { setModal(null) } },
3468
4447
  React.createElement('div', { className: 'dsm-form' },
3469
4448
  React.createElement('label', { className: 'dsm-field' },
3470
4449
  React.createElement('span', { className: 'dsm-label' }, t('subagents.field.name')),
3471
- React.createElement('input', { className: 'dsm-control', value: modal.form.name || '', disabled: modal.mode === 'edit', placeholder: 'code-review', onChange: function (e) { setForm({ name: e.target.value }) } })),
4450
+ React.createElement('input', { className: 'dsm-control', value: modal.form.name || '', placeholder: 'code-review', onChange: function (e) { setForm({ name: e.target.value }) } })),
3472
4451
  React.createElement('label', { className: 'dsm-field' },
3473
4452
  React.createElement('span', { className: 'dsm-label' }, t('subagents.field.description')),
3474
4453
  React.createElement('input', { className: 'dsm-control', value: modal.form.description || '', placeholder: t('subagents.field.description.placeholder'), onChange: function (e) { setForm({ description: e.target.value }) } })),
@@ -3485,6 +4464,7 @@ function callApi(path, options) {
3485
4464
  React.createElement('span', { className: 'dsm-adv-note' },
3486
4465
  modal.advanced ? '' : t('subagents.adv.summary', {
3487
4466
  model: modal.form.model ? (modal.form.provider ? modal.form.provider + '/' + modal.form.model : modal.form.model) : t('subagents.adv.inherit'),
4467
+ modes: Object.keys(modal.form.toolsByPreset || {}).length,
3488
4468
  allow: (modal.form.tools || []).length,
3489
4469
  deny: (modal.form.toolsDeny || []).length,
3490
4470
  }))),
@@ -3521,52 +4501,49 @@ function callApi(path, options) {
3521
4501
  React.createElement('span', { className: 'dsm-label' }, t('subagents.field.provider')),
3522
4502
  React.createElement('input', { className: 'dsm-control', value: modal.form.provider || '', placeholder: t('subagents.field.provider.placeholder'), onChange: function (e) { setForm({ provider: e.target.value }) } }),
3523
4503
  React.createElement('p', { className: 'dsm-help' }, t('subagents.field.provider.hint'))),
3524
- // 工具白名单:勾选 = 子代理只保留这些工具(不勾 = 基础组合全集)。
3525
- toolPicker({
3526
- key: 'allow',
3527
- label: t('subagents.field.tools'),
3528
- hint: t('subagents.field.tools.hint'),
3529
- selected: modal.form.tools || [],
3530
- onToggle: function (name) { setForm({ tools: toggled(modal.form.tools || [], name) }) },
3531
- onClear: function () { setForm({ tools: [] }) },
3532
- onAll: function () { setForm({ tools: (cand.tools || []).map(function (x) { return x.name }) }) },
3533
- chosen: t('subagents.tools.chosen'),
3534
- filterPlaceholder: t('subagents.tools.filter'),
3535
- empty: t('subagents.tools.empty'),
3536
- unavailable: t('subagents.tools.unavailable'),
3537
- current: t('subagents.tools.current'),
3538
- groupCurrent: t('subagents.tools.groupCurrent'),
3539
- groupOther: t('subagents.tools.groupOther'),
3540
- }),
3541
- // 工具黑名单:勾选 = 从子代理可见集合里移除(官方 ToolRestriction.deny,优先级高于白名单)。
3542
- toolPicker({
3543
- key: 'deny',
3544
- label: t('subagents.field.toolsDeny'),
3545
- hint: t('subagents.field.toolsDeny.hint'),
3546
- deny: true,
3547
- selected: modal.form.toolsDeny || [],
3548
- onToggle: function (name) { setForm({ toolsDeny: toggled(modal.form.toolsDeny || [], name) }) },
3549
- onClear: function () { setForm({ toolsDeny: [] }) },
3550
- onAll: function () { setForm({ toolsDeny: (cand.tools || []).map(function (x) { return x.name }) }) },
3551
- chosen: t('subagents.tools.chosen'),
3552
- filterPlaceholder: t('subagents.tools.filter'),
3553
- empty: t('subagents.tools.empty'),
3554
- unavailable: t('subagents.tools.unavailable'),
3555
- current: t('subagents.tools.current'),
3556
- groupCurrent: t('subagents.tools.groupCurrent'),
3557
- groupOther: t('subagents.tools.groupOther'),
3558
- }),
4504
+ // 工具限制:按 Agent 预设一行一个模式(默认全折叠、全部未启动,白/黑互斥)。
4505
+ // 旧格式的全局名单(老文件 / 别处导入)在这里只读呈现,点「转换」才搬进某个模式。
4506
+ legacyNotice(),
4507
+ modeRows(),
3559
4508
  cand.error ? React.createElement('div', { className: 'dsm-feedback dsm-warning' }, String(cand.error)) : null,
3560
4509
  React.createElement('p', { className: 'dsm-adv-note' }, t('subagents.adv.note')))
3561
4510
  : null),
3562
4511
  modal.form.error ? React.createElement('div', { className: 'dsm-feedback dsm-error', role: 'alert' }, String(modal.form.error)) : null),
3563
4512
  React.createElement('div', { className: 'dsm-modal-actions' },
3564
4513
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-primary', disabled: busy || (modal.mode === 'create' && !String(modal.form.name || '').trim()) || !String(modal.form.body || '').trim(), onClick: submitEditor }, t('memory.btn.save')))) : null,
3565
- modal && modal.type === 'delete' ? React.createElement(Modal, { key: 'sdel2', title: t('subagents.delete.title'), closeLabel: t('btn.cancel'), onClose: function () { setModal(null) } },
4514
+ modal && modal.type === 'delete' ? React.createElement(Modal, { key: 'sdel2', title: t('subagents.delete.title'), closeLabel: t('btn.close'), onClose: function () { setModal(null) } },
3566
4515
  React.createElement('p', { className: 'dsm-help' }, t('subagents.delete.desc', { name: modal.name })),
3567
4516
  React.createElement('div', { className: 'dsm-modal-actions' },
3568
4517
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { submitDelete(modal.name) } }, t('memory.btn.delete.confirm')))) : null,
3569
- modal && modal.type === 'import' ? React.createElement(ImportModal, { key: 'simp', t: t, title: t('subagents.import.title'), hint: t('subagents.import.hint'), busy: busy, onClose: function () { setModal(null) }, onSubmit: submitImport }) : null)
4518
+ modal && modal.type === 'import' ? React.createElement(ImportModal, { key: 'simp', t: t, title: t('subagents.import.title'), busy: busy, requirements: [t('upload.requirement.persona.1'), t('upload.requirement.persona.2'), t('upload.requirement.persona.3')], onClose: function () { setModal(null) }, onSubmit: submitImport }) : null,
4519
+ modal && modal.type === 'export' ? React.createElement(ExportModal, {
4520
+ key: 'export', t: t, title: t('export.subagents'),
4521
+ items: data.subagents.map(function (p) { return { key: p.name, name: p.name, desc: p.description || '' } }),
4522
+ busy: busy, result: result,
4523
+ onClose: function () { setResult(null); setModal(null) },
4524
+ onSubmit: function (names, outDir) {
4525
+ setBusy(true); setResult(null);
4526
+ apiCall('bundle-export', { kind: 'subagents', names: names, outDir: outDir }).then(function (res) {
4527
+ setBusy(false);
4528
+ if (res && res.ok) setResult({ ok: true, text: exportResultText(t, res) });
4529
+ else setResult({ ok: false, text: translateError(t, res) });
4530
+ }).catch(function (e) { setBusy(false); setResult({ ok: false, text: String((e && e.message) || e) }) });
4531
+ },
4532
+ }) : null,
4533
+ trash ? React.createElement(TrashModal, {
4534
+ t: t,
4535
+ title: t('trash.title'),
4536
+ groupTitle: t('trash.group.agents'),
4537
+ groupSub: t('trash.section.agents.sub'),
4538
+ locked: anyLocked,
4539
+ entries: trash.entries,
4540
+ loading: trash.loading,
4541
+ error: trash.error,
4542
+ busy: trashBusy,
4543
+ onClose: function () { setTrash(null) },
4544
+ onRestore: restorePersona,
4545
+ onPurge: purgePersona,
4546
+ }) : null)
3570
4547
  }
3571
4548
 
3572
4549
  function MemoryPage() {
@@ -3579,6 +4556,8 @@ function callApi(path, options) {
3579
4556
  })
3580
4557
  var data = state[0], setData = state[1]
3581
4558
  var qs = React.useState('')
4559
+ // 场景锁定(v0.8):任一场景锁定 = 五个域整体冻结;本页全部写控件禁用。
4560
+ var anyLocked = data.anyLocked === true
3582
4561
  var query = qs[0], setQuery = qs[1]
3583
4562
  var sfs = React.useState('')
3584
4563
  var sceneFilter = sfs[0], setSceneFilter = sfs[1]
@@ -3598,6 +4577,10 @@ function callApi(path, options) {
3598
4577
  // 记忆回收站:列表在打开弹窗时按需拉取(rules-trash-list)。
3599
4578
  var trs = React.useState({ loading: false, error: null, entries: [] })
3600
4579
  var trash = trs[0], setTrash = trs[1]
4580
+ // D14:导出弹窗的状态(busy + 结果)。刻意不复用本页的 `result`:
4581
+ // 那个是页面级回执(会自动消失),导出结果只该出现在弹窗里。
4582
+ var exs = React.useState(null)
4583
+ var exportState = exs[0], setExportState = exs[1]
3601
4584
  // bundle 附件的隐藏文件选择器;上限与服务端 MAX_ATTACH_ENTRY_BYTES 对齐。
3602
4585
  var attachRef = React.useRef(null)
3603
4586
  var ATTACH_MAX_MB = 8
@@ -3936,9 +4919,9 @@ function callApi(path, options) {
3936
4919
  r.descriptionDerived ? React.createElement('span', { className: 'dsm-tag' }, t('memory.derived')) : null),
3937
4920
  React.createElement('div', { className: 'dsm-status ' + statusCls }, t(statusKey)),
3938
4921
  React.createElement('div', { className: 'dsm-row-actions' },
3939
- React.createElement(Switch, { on: enabled, disabled: busy || shadowed, label: t('memory.enable') + ' ' + r.name, onClick: function () { toggleRule(r) } }),
3940
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || shadowed, onClick: function () { openEditor(r) } }, t('memory.edit')),
3941
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy || shadowed, onClick: function () { setModal({ type: 'delete', rule: r }) } }, t('memory.delete'))))
4922
+ React.createElement(Switch, { on: enabled, disabled: busy || shadowed || anyLocked, label: t('memory.enable') + ' ' + r.name, onClick: function () { toggleRule(r) } }),
4923
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || shadowed || anyLocked, onClick: function () { openEditor(r) } }, t('memory.edit')),
4924
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy || shadowed || anyLocked, onClick: function () { setModal({ type: 'delete', rule: r }) } }, t('memory.delete'))))
3942
4925
  }
3943
4926
 
3944
4927
  function renderSceneCard(name) {
@@ -3952,7 +4935,7 @@ function callApi(path, options) {
3952
4935
  return React.createElement('div', { key: 's:' + name, className: 'dsm-source' + (meta.active === false || isOrphan ? ' dsm-rule-shadowed' : '') },
3953
4936
  React.createElement('div', { className: 'dsm-source-head' },
3954
4937
  React.createElement('button', { type: 'button', className: 'dsm-source-head-main', 'aria-expanded': open, onClick: function () { toggleCollapse('s:' + name) } },
3955
- React.createElement('span', { className: 'dsm-source-title' }, sceneLabel(data.scenes, name)),
4938
+ React.createElement('span', { className: 'dsm-source-title', title: sceneLabel(data.scenes, name) }, sceneLabel(data.scenes, name)),
3956
4939
  React.createElement('span', { className: 'dsm-count' }, t('memory.scene.count', { count: bucket.rules.length })),
3957
4940
  isShared ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('memory.scene.shared')) : null,
3958
4941
  isGlobal ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('memory.scene.global.tag')) : null,
@@ -3974,19 +4957,42 @@ function callApi(path, options) {
3974
4957
  : React.createElement('div', { className: 'dsm-empty' }, t('memory.scene.empty'))) : null)
3975
4958
  }
3976
4959
 
4960
+ // D14 导出:可导出的记忆 = 索引里的规则,排除被同名 bundle 遮蔽的 flat
4961
+ // (它们不会被加载,导出去只会让人以为能用)。key 用**记忆 id**:
4962
+ // 服务端按 id 在索引里查真实文件路径(bundle 型必须这么做,见 planMemoryExport)。
4963
+ var exportableRules = (data.rules || []).filter(function (r) { return r.shadowed !== true })
4964
+ /** 导出条目:按**场景**分组(记忆页的分组维度就是场景),组内给出名称 + 描述。 */
4965
+ function memoryExportItems() {
4966
+ var sceneMeta = {}
4967
+ ;(data.scenes || []).forEach(function (s) { sceneMeta[s.name] = s })
4968
+ return exportableRules.map(function (r) {
4969
+ var scene = sceneOf(r.group)
4970
+ var meta = sceneMeta[scene]
4971
+ var label = (meta && (meta.label || meta.name)) || scene || t('memory.scene.orphan')
4972
+ return { key: r.id, name: r.name, desc: r.description || '', group: label }
4973
+ })
4974
+ }
4975
+ function submitExport(names, outDir) {
4976
+ setExportState({ busy: true, result: null })
4977
+ apiCall('bundle-export', { kind: 'memories', names: names, outDir: outDir }).then(function (res) {
4978
+ if (res && res.ok) setExportState({ busy: false, result: { ok: true, text: exportResultText(t, res) } })
4979
+ else setExportState({ busy: false, result: { ok: false, text: translateError(t, res) } })
4980
+ }).catch(function (e) { setExportState({ busy: false, result: { ok: false, text: String((e && e.message) || e) } }) })
4981
+ }
4982
+
3977
4983
  return React.createElement('section', { className: 'dsm-section' },
3978
4984
  React.createElement('div', { className: 'dsm-head' },
3979
4985
  React.createElement('div', { className: 'dsm-title-block' },
3980
4986
  React.createElement('div', { className: 'dsm-title-row' },
3981
- React.createElement('h2', { className: 'dsm-title' }, t('memory.title')),
3982
- VersionBadgeComponent ? React.createElement(VersionBadgeComponent, null) : null),
4987
+ React.createElement('h2', { className: 'dsm-title' }, t('memory.title'))),
3983
4988
  React.createElement('p', { className: 'dsm-desc' }, t('memory.desc'))),
3984
4989
  React.createElement('div', { className: 'dsm-actions' },
3985
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || data.loading, onClick: function () { refresh() } }, t('memory.btn.refresh')),
3986
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: openTrash }, t('memory.trash.open')),
3987
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: function () { setResult(null); setImportScene(''); setModal({ type: 'import' }) } }, t('memory.import')),
3988
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: function () { openCreate(sceneFilter || '') } }, t('memory.btn.new')))),
3989
- React.createElement('div', { className: 'dsm-summary dsm-summary-3' },
4990
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || data.loading, onClick: function () { refresh() } }, (busy || data.loading) ? t('btn.refreshing') : t('btn.refresh')),
4991
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || anyLocked, onClick: function () { openCreate(sceneFilter || '') } }, t('memory.btn.new')),
4992
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || anyLocked, onClick: function () { setResult(null); setImportScene(''); setModal({ type: 'import' }) } }, t('memory.import')),
4993
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || !exportableRules.length, onClick: function () { setResult(null); setExportState({ busy: false, result: null }) } }, t('export.memories')),
4994
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: openTrash }, t('memory.trash.open')))),
4995
+ React.createElement('div', { className: 'dsm-summary' },
3990
4996
  React.createElement('div', { key: 'total', className: 'dsm-stat' }, React.createElement('strong', null, stats.total || 0), t('memory.stat.total')),
3991
4997
  React.createElement('div', { key: 'scenes', className: 'dsm-stat' }, React.createElement('strong', null, (data.scenes || []).length), t('memory.stat.sceneCount')),
3992
4998
  React.createElement('div', { key: 'active', className: 'dsm-stat' }, React.createElement('strong', null, stats.scenes || 0), t('memory.stat.scenes'))),
@@ -4010,20 +5016,27 @@ function callApi(path, options) {
4010
5016
  data.loading && !hasAnyRule ? React.createElement('div', { key: 'loading', className: 'dsm-empty' }, t('memory.loading'))
4011
5017
  : visibleBuckets.length ? React.createElement('div', { key: 'scenes', className: 'dsm-sources' }, visibleBuckets.map(renderSceneCard))
4012
5018
  : React.createElement('div', { key: 'empty', className: 'dsm-empty' }, q || sceneFilter ? t('memory.empty.search') : t('memory.empty')),
4013
- modal && modal.type === 'delete' ? React.createElement(Modal, { key: 'del', title: t('memory.delete.title'), closeLabel: t('btn.cancel'), onClose: function () { setModal(null) } },
5019
+ modal && modal.type === 'delete' ? React.createElement(Modal, { key: 'del', title: t('memory.delete.title'), closeLabel: t('btn.close'), onClose: function () { setModal(null) } },
4014
5020
  React.createElement('p', { className: 'dsm-help' }, t('memory.delete.desc', { name: modal.rule.name })),
4015
5021
  React.createElement('div', { className: 'dsm-modal-actions' },
4016
5022
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { submitDelete(modal.rule) } }, t('memory.btn.delete.confirm')))) : null,
4017
5023
  modal && modal.type === 'import' ? React.createElement(ImportModal, {
4018
- key: 'mimp', t: t, title: t('memory.import.title'), hint: t('memory.import.hint'), busy: busy,
5024
+ key: 'mimp', t: t, title: t('memory.import.title'), busy: busy,
5025
+ requirements: [t('upload.requirement.memory.1'), t('upload.requirement.memory.2'), t('upload.requirement.memory.3')],
4019
5026
  extra: React.createElement('label', { className: 'dsm-field' },
4020
5027
  React.createElement('span', { className: 'dsm-label' }, t('memory.import.scene')),
4021
- React.createElement('input', { className: 'dsm-control', value: importScene, placeholder: t('memory.scene.global.hint'), onChange: function (e) { setImportScene(e.target.value) } }),
4022
- React.createElement('p', { className: 'dsm-help' }, t('memory.import.sceneHint'))),
5028
+ React.createElement('input', { className: 'dsm-control', value: importScene, placeholder: t('memory.scene.global.hint'), onChange: function (e) { setImportScene(e.target.value) } })),
4023
5029
  onClose: function () { setModal(null) },
4024
5030
  onSubmit: submitImport,
4025
5031
  }) : null,
4026
- editor && modal && modal.type === 'editor' ? React.createElement(Modal, { key: 'editor', className: 'dsm-modal-wide', title: editor.mode === 'create' ? t('memory.create.title') : t('memory.edit.title'), closeLabel: t('btn.cancel'), onClose: closeEditor },
5032
+ exportState ? React.createElement(ExportModal, {
5033
+ key: 'mexport', t: t, title: t('export.memories'),
5034
+ items: memoryExportItems(),
5035
+ busy: exportState.busy, result: exportState.result,
5036
+ onClose: function () { setExportState(null) },
5037
+ onSubmit: submitExport,
5038
+ }) : null,
5039
+ editor && modal && modal.type === 'editor' ? React.createElement(Modal, { key: 'editor', className: 'dsm-modal-wide', title: editor.mode === 'create' ? t('memory.create.title') : t('memory.edit.title'), closeLabel: t('btn.close'), onClose: closeEditor },
4027
5040
  React.createElement('div', { className: 'dsm-form' },
4028
5041
  React.createElement('label', { className: 'dsm-field' },
4029
5042
  React.createElement('span', { className: 'dsm-label' }, t('memory.field.group')),
@@ -4093,7 +5106,7 @@ function callApi(path, options) {
4093
5106
  trash.entries.length
4094
5107
  ? React.createElement(React.Fragment, null,
4095
5108
  React.createElement('div', { className: 'dsm-count' }, t('memory.trash.count', { count: trash.entries.length })),
4096
- trash.entries.map(function (entry) {
5109
+ React.createElement('div', { className: 'dsm-trash-box' }, trash.entries.map(function (entry) {
4097
5110
  return React.createElement('div', { key: entry.trashId, className: 'dsm-trash-item' },
4098
5111
  React.createElement('div', { className: 'dsm-trash-main' },
4099
5112
  React.createElement('div', { className: 'dsm-name' }, entry.name),
@@ -4102,9 +5115,9 @@ function callApi(path, options) {
4102
5115
  + t('memory.trash.deletedAt', { time: entry.deletedAt ? new Date(entry.deletedAt).toLocaleString() : '' }))),
4103
5116
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { submitRestore(entry) } }, t('memory.trash.restore')),
4104
5117
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy, onClick: function () { setModal({ type: 'trash-delete', entry: entry }) } }, t('memory.trash.purge')))
4105
- }))
5118
+ })))
4106
5119
  : React.createElement('div', { className: 'dsm-empty' }, t('memory.trash.empty')))) : null,
4107
- modal && modal.type === 'trash-delete' ? React.createElement(Modal, { key: 'trash-delete', title: t('memory.trash.confirmTitle'), closeLabel: t('btn.cancel'), onClose: function () { setModal({ type: 'trash' }) } },
5120
+ modal && modal.type === 'trash-delete' ? React.createElement(Modal, { key: 'trash-delete', title: t('memory.trash.confirmTitle'), closeLabel: t('btn.close'), onClose: function () { setModal({ type: 'trash' }) } },
4108
5121
  React.createElement('p', { className: 'dsm-help' }, t('memory.trash.confirmDesc', { name: modal.entry.name })),
4109
5122
  React.createElement('div', { className: 'dsm-modal-actions' },
4110
5123
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { submitTrashDelete(modal.entry) } }, t('memory.trash.purge')))) : null)