Forráskód Böngészése

refactor(web): separate preset management from global plugins

Yichen Jiang 3 hete
szülő
commit
6a6796f48d
100 módosított fájl, 567 hozzáadás és 2437 törlés
  1. 0 18
      .agents/notes/implemented/architecture/2026-09-04-plugin-management-in-web-settings.md
  2. 0 18
      .agents/notes/implemented/architecture/2026-09-04-plugin-management-in-web-settings.zh.md
  3. 2 2
      .agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.i18n.yaml
  4. 2 2
      .agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.md
  5. 2 2
      .agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.zh.md
  6. 0 6
      .agents/notes/implemented/architecture/2026-09-04-preset-user-patch-layer-and-patch-file-writer.i18n.yaml
  7. 0 35
      .agents/notes/implemented/architecture/2026-09-04-preset-user-patch-layer-and-patch-file-writer.md
  8. 0 35
      .agents/notes/implemented/architecture/2026-09-04-preset-user-patch-layer-and-patch-file-writer.zh.md
  9. 3 3
      .agents/notes/implemented/architecture/2026-09-04-profile-patch-file-writer.i18n.yaml
  10. 25 0
      .agents/notes/implemented/architecture/2026-09-04-profile-patch-file-writer.md
  11. 25 0
      .agents/notes/implemented/architecture/2026-09-04-profile-patch-file-writer.zh.md
  12. 0 6
      .agents/notes/implemented/architecture/2026-09-04-settings-namespaces-resolve-per-scope.i18n.yaml
  13. 0 37
      .agents/notes/implemented/architecture/2026-09-04-settings-namespaces-resolve-per-scope.md
  14. 0 37
      .agents/notes/implemented/architecture/2026-09-04-settings-namespaces-resolve-per-scope.zh.md
  15. 2 2
      .agents/notes/implemented/architecture/2026-09-09-plugin-management-in-the-web-sidebar.i18n.yaml
  16. 3 3
      .agents/notes/implemented/architecture/2026-09-09-plugin-management-in-the-web-sidebar.md
  17. 3 3
      .agents/notes/implemented/architecture/2026-09-09-plugin-management-in-the-web-sidebar.zh.md
  18. 0 15
      apps/web/tests/expected/agent-preset-authoring/created.expected.md
  19. 0 18
      apps/web/tests/expected/agent-preset-authoring/damaged.expected.md
  20. 0 12
      apps/web/tests/expected/agent-preset-authoring/section.expected.md
  21. 1 1
      apps/web/tests/expected/plugin-manager/manager.expected.md
  22. 0 116
      apps/web/tests/expected/plugin-manager/preset-detail.expected.md
  23. 2 4
      apps/web/tests/fixtures/assembled-remote.fixture.json
  24. 10 105
      apps/web/tests/plugin-manager.e2e.ts
  25. 2 2
      docs/config-catalog.i18n.yaml
  26. 3 3
      docs/config-catalog.md
  27. 3 3
      docs/config-catalog.zh.md
  28. 2 2
      docs/event-producer-consumer.i18n.yaml
  29. 4 4
      docs/event-producer-consumer.md
  30. 2 2
      docs/event-producer-consumer.zh.md
  31. 2 2
      docs/module-graph.i18n.yaml
  32. 10 15
      docs/module-graph.md
  33. 10 15
      docs/module-graph.zh.md
  34. 2 2
      docs/subsystems/core.i18n.yaml
  35. 5 30
      docs/subsystems/core.md
  36. 5 30
      docs/subsystems/core.zh.md
  37. 2 2
      docs/subsystems/settings.i18n.yaml
  38. 50 108
      docs/subsystems/settings.md
  39. 50 108
      docs/subsystems/settings.zh.md
  40. 2 2
      packages/api/settings-controller/README.i18n.yaml
  41. 1 1
      packages/api/settings-controller/README.md
  42. 1 1
      packages/api/settings-controller/README.zh.md
  43. 0 2
      packages/api/settings-controller/package.json
  44. 18 41
      packages/api/settings-controller/src/index.ts
  45. 0 44
      packages/api/settings-controller/tests/settings-controller.host.spec.ts
  46. 0 3
      packages/api/settings-controller/tsconfig.json
  47. 2 2
      packages/boot/app-boot/README.i18n.yaml
  48. 1 1
      packages/boot/app-boot/README.md
  49. 1 1
      packages/boot/app-boot/README.zh.md
  50. 2 2
      packages/boot/plugin-manager/README.i18n.yaml
  51. 5 9
      packages/boot/plugin-manager/README.md
  52. 5 9
      packages/boot/plugin-manager/README.zh.md
  53. 1 1
      packages/boot/plugin-manager/src/index.ts
  54. 38 96
      packages/boot/plugin-manager/src/manager.ts
  55. 3 10
      packages/boot/plugin-manager/src/types.ts
  56. 25 59
      packages/boot/plugin-manager/tests/plugin-manager.spec.ts
  57. 2 2
      packages/client/ui-agent-preset/README.i18n.yaml
  58. 1 1
      packages/client/ui-agent-preset/README.md
  59. 2 2
      packages/client/ui-agent-preset/README.zh.md
  60. 0 76
      packages/client/ui-agent-preset/src/client/AgentPresetSection.module.css
  61. 3 46
      packages/client/ui-agent-preset/src/client/AgentPresetSection.tsx
  62. 0 5
      packages/client/ui-agent-preset/src/client/index.ts
  63. 0 5
      packages/client/ui-agent-preset/src/client/locales.ts
  64. 0 25
      packages/client/ui-agent-preset/src/client/slot-contract.ts
  65. 0 5
      packages/client/ui-agent-preset/tests/apply.client.spec.ts
  66. 1 39
      packages/client/ui-agent-preset/tests/section.client.spec.tsx
  67. 0 1
      packages/client/ui-permission-presets/tests/permission-presets-row.client.spec.tsx
  68. 0 1
      packages/client/ui-permission-presets/tests/settings-store.client.spec.ts
  69. 2 2
      packages/client/ui-plugin-manager/README.i18n.yaml
  70. 7 7
      packages/client/ui-plugin-manager/README.md
  71. 7 7
      packages/client/ui-plugin-manager/README.zh.md
  72. 0 2
      packages/client/ui-plugin-manager/package.json
  73. 1 4
      packages/client/ui-plugin-manager/src/client/NoticeLine.tsx
  74. 1 2
      packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css
  75. 34 117
      packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx
  76. 0 181
      packages/client/ui-plugin-manager/src/client/PresetPluginsSection.tsx
  77. 5 23
      packages/client/ui-plugin-manager/src/client/index.ts
  78. 7 115
      packages/client/ui-plugin-manager/src/client/locales.ts
  79. 19 37
      packages/client/ui-plugin-manager/src/client/manager-store.ts
  80. 6 68
      packages/client/ui-plugin-manager/src/client/presentation.ts
  81. 0 18
      packages/client/ui-plugin-manager/tests/browser-plugin.client.spec.tsx
  82. 47 126
      packages/client/ui-plugin-manager/tests/components.client.spec.tsx
  83. 23 33
      packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts
  84. 0 209
      packages/client/ui-plugin-manager/tests/preset-section.client.spec.tsx
  85. 33 13
      packages/client/ui-plugin-manager/tsconfig.json
  86. 1 2
      packages/client/ui-settings-general/tests/apply.client.spec.ts
  87. 3 4
      packages/client/ui-settings-models/tests/apply.client.spec.ts
  88. 0 6
      packages/client/ui-settings-models/tests/components.client.spec.tsx
  89. 0 1
      packages/client/ui-settings-models/tests/onboarding-dialog.client.spec.tsx
  90. 0 1
      packages/client/ui-settings-models/tests/provider-form.client.spec.tsx
  91. 2 2
      packages/client/ui-settings-plugins/README.i18n.yaml
  92. 4 8
      packages/client/ui-settings-plugins/README.md
  93. 4 8
      packages/client/ui-settings-plugins/README.zh.md
  94. 1 4
      packages/client/ui-settings-plugins/package.json
  95. 0 1
      packages/client/ui-settings-plugins/src/client/AgentLoopCard.tsx
  96. 0 2
      packages/client/ui-settings-plugins/src/client/BashCard.tsx
  97. 14 19
      packages/client/ui-settings-plugins/src/client/ConfigurablePluginsTab.tsx
  98. 0 65
      packages/client/ui-settings-plugins/src/client/PresetSettingsSection.tsx
  99. 0 48
      packages/client/ui-settings-plugins/src/client/SkillFilesystemCard.tsx
  100. 0 2
      packages/client/ui-settings-plugins/src/client/WebSearchCard.tsx

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 18
.agents/notes/implemented/architecture/2026-09-04-plugin-management-in-web-settings.md


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 18
.agents/notes/implemented/architecture/2026-09-04-plugin-management-in-web-settings.zh.md


+ 2 - 2
.agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.md
-2026-09-04-plugin-manager-over-the-profile-runtime.md: e5b3e697e56ffe008556bb112565cd0f83941ee7
-2026-09-04-plugin-manager-over-the-profile-runtime.zh.md: 4661241c23210b09e483c558348054fa81e84677
+2026-09-04-plugin-manager-over-the-profile-runtime.md: c68d7aedcd93ca4f179ab9df97e157ff5388b407
+2026-09-04-plugin-manager-over-the-profile-runtime.zh.md: e01301a5f9e1f35e07471e86fb0df0e223c7f965

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.md

@@ -10,7 +10,7 @@ The CLI and Web need the same installation checks, while only a running applicat
 
 ## Decision
 
-**One business manager, one Remote adapter.** `dsh-plugin-manager` belongs beside app-boot. `PluginInstaller` operates on profile files and pnpm without a running Loader; `PluginManager` adds bundle and row operations over the live profile. The CLI shares the installer. `dsh-host-plugin-manager` supplies per-call readers for the profile runtime, preset layers and running-agent count, relays methods, and maps `PluginOperationError` codes to Remote errors. Missing profile runtime reports `plugins/unavailable` when called. The manager depends on the preset-layer interface, not the preset or agent implementations.
+**One business manager, one Remote adapter.** `dsh-plugin-manager` belongs beside app-boot. `PluginInstaller` operates on profile files and pnpm without a running Loader; `PluginManager` adds bundle and row operations over the live profile. The CLI shares the installer. `dsh-host-plugin-manager` supplies per-call readers for the profile runtime and running-agent count, relays methods, and maps `PluginOperationError` codes to Remote errors. Missing profile runtime reports `plugins/unavailable` when called. The manager reads running-agent counts without depending on the agent implementation.
 
 **One mutation at a time.** An overlapping mutation receives `plugins/busy` rather than entering a queue with stale assumptions. Installation and removal reject with `plugins/agents-running` while an agent is running because pnpm rewrites modules those agents import. Bundle and row edits leave `node_modules` alone and do not use that guard. Every operation reads the profile manifest afresh: dependencies record installation, and `dsh.profile.bundles` records enablement.
 
@@ -18,7 +18,7 @@ The CLI and Web need the same installation checks, while only a running applicat
 
 **Enablement selects a layer; diagnostics describe actual rows.** Bundle enablement, nontransactional recomposition and explicit `dsh.plugins` discovery follow [native entry diagnostics and static declarations](2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.md). The manager keeps per-row issues distinct from the enabled choice. `retry` removes the complete layer, awaits cleanup, and adds it again; unchanged row options alone do not restart a failed plugin. `list` derives each package view from the manifest, static declarations and current Loader state.
 
-**Rows go through the patch-file writer.** `addRow` inserts the declared module and config into the profile user patch or the preset overlay returned by its roster. `setRowDisabled` writes or removes `disabled: true`, restoring the author's original gate when the user withdraws a denial. Global changes recompose the live tree; preset changes affect subsequent standing generations.
+**Rows go through the patch-file writer.** `addRow` inserts the declared module and config into the profile user patch. `setRowDisabled` writes or removes `disabled: true`, restoring the author's original gate when the user withdraws a denial. Global changes recompose the live tree.
 
 **Runtime notifications follow diagnostics.** Native entry and fiber events share one pending reader. It waits for Loader and profile recomposition, then compares row identity, module, fiber phase, failure stage and message. Only a changed diagnostic set emits `plugins/changed` with reason `runtime`; healthy status churn and repeated identical failures stay silent. Management operations retain their own completion notifications. Events arriving during a read request another pass, and disposing the adapter cancels publication.
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.zh.md

@@ -10,7 +10,7 @@ CLI 与 Web 需要相同的安装检查,而只有运行中的应用才能应
 
 ## Decision
 
-**一个业务管理器,一个 Remote 适配器。** `dsh-plugin-manager` 位于 app-boot 旁。`PluginInstaller` 操作 profile 文件和 pnpm,不需要运行中的 Loader;`PluginManager` 在其上增加面向运行中 profile 的组合包与行操作。CLI 共用安装器。`dsh-host-plugin-manager` 提供逐次调用的 profile runtime、预设层与运行中 agent 数读取器,转接方法并将 `PluginOperationError` 错误码映射为 Remote 错误。缺少 profile runtime 时在调用处报告 `plugins/unavailable`。管理器依赖预设层接口,不依赖预设或 agent 的实现。
+**一个业务管理器,一个 Remote 适配器。** `dsh-plugin-manager` 位于 app-boot 旁。`PluginInstaller` 操作 profile 文件和 pnpm,不需要运行中的 Loader;`PluginManager` 在其上增加面向运行中 profile 的组合包与行操作。CLI 共用安装器。`dsh-host-plugin-manager` 提供逐次调用的 profile runtime 与运行中 agent 数读取器,转接方法并将 `PluginOperationError` 错误码映射为 Remote 错误。缺少 profile runtime 时在调用处报告 `plugins/unavailable`。管理器读取运行中 agent 的数量,不依赖 agent 实现。
 
 **同时只允许一个变更。** 重叠变更收到 `plugins/busy`,不进入可能携带过期假设的队列。有 agent 运行时,安装与移除以 `plugins/agents-running` 拒绝,因为 pnpm 会重写这些 agent 导入的模块。组合包与行编辑不改 `node_modules`,不使用这一限制。每次操作重新读取 profile 清单:依赖记录安装,`dsh.profile.bundles` 记录启用。
 
@@ -18,7 +18,7 @@ CLI 与 Web 需要相同的安装检查,而只有运行中的应用才能应
 
 **启用选择层,诊断描述实际行。** 组合包启用、非事务重组和显式 `dsh.plugins` 发现遵循[原生条目诊断与静态声明](2026-09-11-native-entry-diagnostics-and-static-plugin-declarations.zh.md)。管理器区分逐行问题与启用选择。`retry` 移除完整层,等待清理,再将其加入;仅保留不变的行选项不会重启失败插件。`list` 从清单、静态声明和当前 Loader 状态派生每个包的视图。
 
-**行通过 patch-file 写入器修改。** `addRow` 将声明的模块与配置插入 profile 用户 patch 或 roster 返回的预设覆盖层。`setRowDisabled` 写入或移除 `disabled: true`,用户撤销禁用时恢复作者原来的条件。全局修改重组运行中的树;预设修改影响后续常驻代际。
+**行通过 patch-file 写入器修改。** `addRow` 将声明的模块与配置插入 profile 用户 patch。`setRowDisabled` 写入或移除 `disabled: true`,用户撤销禁用时恢复作者原来的条件。全局修改重组运行中的树。
 
 **运行时通知跟随诊断。** 原生条目与 fiber 事件共用一次待完成的读取。读取等待 Loader 和 profile 重组完成,再比较行身份、模块、fiber 阶段、失败阶段与消息。只有诊断集合变化才发送原因是 `runtime` 的 `plugins/changed`;健康状态波动和重复的相同失败保持安静。管理操作仍保留自己的完成通知。读取期间到来的事件请求再次读取,适配器销毁时取消发布。
 

+ 0 - 6
.agents/notes/implemented/architecture/2026-09-04-preset-user-patch-layer-and-patch-file-writer.i18n.yaml

@@ -1,6 +0,0 @@
-# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
-# side as of the last confirmed-consistent state. Both languages carry equal authority;
-# after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-04-preset-user-patch-layer-and-patch-file-writer.md
-2026-09-04-preset-user-patch-layer-and-patch-file-writer.md: 78dbea52069d50c5772d47e72ab7b0a963ea21a5
-2026-09-04-preset-user-patch-layer-and-patch-file-writer.zh.md: 8129ebeb7a6c01feb2cbdd5872199460db8b44a5

+ 0 - 35
.agents/notes/implemented/architecture/2026-09-04-preset-user-patch-layer-and-patch-file-writer.md

@@ -1,35 +0,0 @@
-# Agent Note: A preset takes a user patch layer, and patch files get one home
-
-Status: implemented
-
-English | [中文](2026-09-04-preset-user-patch-layer-and-patch-file-writer.zh.md)
-
-## Problem
-
-A shipped agent preset was read-only in every sense: to switch one of its rows off or add a tool to it, a person copied the whole preset and edited the copy, which then drifted from the shipped one on every upgrade. The profile already had the shape that solves this — a base composition plus a `cordis.patch.yml` user layer applied over it — and the preset had nothing of the kind. Separately, that file format had one parser in `dsh-app-boot` and no writer: a program that wanted to switch a row off had to rewrite the whole file, losing the author's comments and any `!!js` gate it did not understand.
-
-## Decision
-
-**A preset's user layer is `cordis.patch.yml` in its slot.** Beside the composition for a locally authored preset; alone in the user root's directory of the same id (`$DSH_HOME/.agent-presets/<id>/cordis.patch.yml`) for a shipped one, whose install stays untouched. Discovery attaches the layer to the preset that wins the id, judges it with the composition (an unparsable layer, a malformed insert, or an inserted row naming a module that cannot resolve makes the preset broken with that reason), and reports a layer whose id no root supplies as a broken slot rather than hiding it. The mount hands the parsed layer to the include as its runtime patches, so it applies with the Loader's own patch semantics. The composition inventory applies it before flattening and marks each row `source: 'preset' | 'user'` and, when off, `disabledBy: 'composition' | 'user'`. A copy carries the layer in beside the new composition; `removeOverlay` deletes it and an emptied slot.
-
-**Generations follow the layer's content.** The standing mount's stamp now covers the composition file's stat and a digest of the layer's text. A changed layer starts the next generation for sessions created afterwards, as an edited composition always did; a layer edited back to a content an earlier generation composed returns to that generation, so switching a row off and on again does not stack a third live subtree.
-
-**`dsh-app-boot/patch-file` owns the format.** The parser and the writer are one module of `dsh-app-boot`, exported as `./patch-file` so the roster and the plugin manager load them without the boot entry; `parsePatchList` is the one parser (js-yaml with the include's `!!js` dialect, relative names anchored to the file); `PatchDocument` edits a file at the key level through the `yaml` package's comment-preserving document, which keeps an unresolved `!!js` tag on its scalar and prints it back verbatim; `mutatePatchFile` takes the `dsh-atomic-write` lock, reads, edits, replaces atomically when the document is dirty, and parses the written text back. A removed row's comment block moves to its neighbour or the document's trailing comment instead of vanishing.
-
-## Alternatives considered
-
-**A settings namespace per preset for enablement.** Rejected earlier in the design: enablement is composition, not preference, and the profile's own user layer is already a patch file; a preset's should be the same file in the same format.
-
-**Editing the shipped composition in place.** Rejected: an upgrade overwrites it, and a session already running would be the one reading a half-written file.
-
-**A second parser in `dsh-agent-presets` to avoid a dependency on `dsh-app-boot`.** Rejected: two parsers of one format drift. A separate `dsh-patch-file` util package was tried first and folded back into `dsh-app-boot` as its `./patch-file` export: the format is the Loader's and the include's, not a mechanical primitive, and a dozen plugin packages already depend on `dsh-app-boot`, so the roster joining them costs nothing.
-
-**A `dsh-global-tool-mask` row: `tools.restrict()` as a composition row, so a preset's layer could hide a host tool.** Built, then removed before merge: no shipped preset, the plugin manager, or the settings UI composes one, so it was a package with a README and no consumer. A layer adds rows and switches the composition's rows off; hiding a host tool from one preset is a per-preset setting for the scope-resolved settings of the next layer, not a hand-written row.
-
-## Consequences
-
-A person hides a tool from `standard` with three lines under `.agent-presets/standard/` and keeps the shipped composition; the plugin manager writes the same file for them. A layer applies to the sessions created after it changes, never to running ones. The layer addresses rows by the composition's own ids, so an id the composition leaves anonymous cannot be targeted.
-
-## Testing
-
-`packages/preset/agent-presets/tests/overlay.spec.ts` pins discovery (attach, own layer, orphan slot, unparsable, malformed, unresolvable insert, composition verdict first), the mount applying the layer, generations across edits with the retired generation reused, the inventory's `source` and `disabledBy` from the file and from a mount, the layer path for shipped and authored presets, copy carrying the layer, and removal. `packages/boot/app-boot/tests/patch-file.spec.ts` pins the parser, the document edits with comments and `!!js` intact, and the locked atomic mutation with readback.

+ 0 - 35
.agents/notes/implemented/architecture/2026-09-04-preset-user-patch-layer-and-patch-file-writer.zh.md

@@ -1,35 +0,0 @@
-# Agent Note:preset 获得用户补丁层,补丁文件有了唯一的家
-
-Status: implemented
-
-[English](2026-09-04-preset-user-patch-layer-and-patch-file-writer.md) | 中文
-
-## 问题
-
-随附的 agent preset 在任何意义上都是只读的:想关掉它的一行或给它加一个工具,只能整个复制 preset 再改副本,而副本从此在每次升级时与随附版本渐行渐远。profile 早已具备解决这件事的形状——一份基底组合加一层施加其上的 `cordis.patch.yml` 用户层——而 preset 没有任何对应物。另一方面,这种文件格式在 `dsh-app-boot` 里只有一个解析器而没有写入器:想关掉一行的程序只能整个改写文件,丢掉作者的注释和它看不懂的 `!!js` 门。
-
-## 决定
-
-**preset 的用户层就是其槽位里的 `cordis.patch.yml`。** 本地创作的 preset 放在组合旁边;随附 preset 则单独放在用户根目录里同 id 的目录(`$DSH_HOME/.agent-presets/<id>/cordis.patch.yml`),其安装目录不动。discovery 把这一层挂到赢得该 id 的 preset 上,与组合一起判定健康(不可解析的层、畸形的 insert、或插入了无法解析模块的行,都以该原因让 preset 变为 broken),并把没有任何根提供其 id 的层报告为损坏的槽位而不是藏起来。mount 把解析后的层作为 include 的运行时 patches 交上去,于是它以 Loader 自己的补丁语义施加。组合清单在展平前先施加它,并给每一行标上 `source: 'preset' | 'user'`,关闭时再标上 `disabledBy: 'composition' | 'user'`。复制会把该层带到新组合旁边;`removeOverlay` 删除它以及被清空的槽位。
-
-**代际跟随层的内容。** 常驻挂载的印记现在覆盖组合文件的 stat 与层文本的摘要。层变化即为之后创建的会话开启下一代,一如组合被编辑时的既有行为;层被改回某个早先代际组合过的内容时,回到那一代,因此一行关掉再打开不会堆出第三棵在线子树。
-
-**`dsh-app-boot/patch-file` 拥有这种格式。** 解析器与写入器是 `dsh-app-boot` 的同一个模块,以 `./patch-file` 导出,roster 与插件管理器加载它时不经过启动入口;`parsePatchList` 是唯一的解析器(js-yaml 配 include 的 `!!js` 方言,相对名字锚定到文件);`PatchDocument` 通过 `yaml` 包保留注释的 document 在键级编辑文件,它把未解析的 `!!js` 标签留在标量上并原样打印;`mutatePatchFile` 拿 `dsh-atomic-write` 的锁,读取、编辑、在 document 变脏时原子替换,并把写出的文本回读解析。被移除行上方的注释块移到邻居或文档尾注释,而不是消失。
-
-## 考虑过的替代方案
-
-**为每个 preset 用一个 settings 命名空间做启停。** 在设计阶段已否决:启停是组合而非偏好,profile 自己的用户层本就是补丁文件;preset 的应当是同样格式的同一种文件。
-
-**原地编辑随附组合。** 否决:升级会覆盖它,而正在运行的会话会读到写了一半的文件。
-
-**在 `dsh-agent-presets` 里再写一个解析器以避免依赖 `dsh-app-boot`。** 否决:同一格式的两个解析器会漂移。曾先做成独立的 `dsh-patch-file` util 包,后折回 `dsh-app-boot` 作为它的 `./patch-file` 导出:这种格式属于 Loader 与 include,不是机械原语,而且已有十几个插件包依赖 `dsh-app-boot`,roster 加入其中不增加任何代价。
-
-**一个 `dsh-global-tool-mask` 行:把 `tools.restrict()` 写成组合行,让 preset 的层能遮蔽宿主工具。** 做出来后在合并前移除:随附 preset、插件管理器与设置 UI 都不会组合它,它是一个只有 README 没有消费者的包。层负责加行和关掉组装自己的行;对某个 preset 隐藏宿主工具属于下一层按 scope 解析的设置里的一条 preset 级设置,不是手写的行。
-
-## 后果
-
-一个人用 `.agent-presets/standard/` 下的三行就能对 `standard` 隐藏一个工具并保留随附组合;插件管理器替他们写同一个文件。层作用于其变化之后创建的会话,从不作用于运行中的会话。层按组合自己的 id 寻址行,因此组合留作匿名的行无法被定位。
-
-## 测试
-
-`packages/preset/agent-presets/tests/overlay.spec.ts` 钉住 discovery(附着、自有层、孤儿槽位、不可解析、畸形、不可解析的插入、组合判定优先)、施加层的挂载、编辑之间的代际及退役代际的复用、清单从文件与从挂载得到的 `source` 与 `disabledBy`、随附与自作 preset 的层路径、复制携带层,以及移除。`packages/boot/app-boot/tests/patch-file.spec.ts` 钉住解析器、保留注释与 `!!js` 的文档编辑,以及带回读的加锁原子变更。

+ 3 - 3
.agents/notes/implemented/architecture/2026-09-04-plugin-management-in-web-settings.i18n.yaml → .agents/notes/implemented/architecture/2026-09-04-profile-patch-file-writer.i18n.yaml

@@ -1,6 +1,6 @@
 # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-04-plugin-management-in-web-settings.md
-2026-09-04-plugin-management-in-web-settings.md: 29aff42ae6fafad2e1d7a20362676722ea6228df
-2026-09-04-plugin-management-in-web-settings.zh.md: 0f8de3fc9106e55cc651e769e76d0bb2a542b033
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-04-profile-patch-file-writer.md
+2026-09-04-profile-patch-file-writer.md: a7c8e5f10b33b0471e6d9fc225a7432a1ea14164
+2026-09-04-profile-patch-file-writer.zh.md: 57d73bdee6ce77afd0fd1a1551c1a39001891be9

+ 25 - 0
.agents/notes/implemented/architecture/2026-09-04-profile-patch-file-writer.md

@@ -0,0 +1,25 @@
+# Agent Note: Profile patch files share one parser and writer
+
+Status: implemented
+
+English | [中文](2026-09-04-profile-patch-file-writer.zh.md)
+
+## Problem
+
+The launcher and plugin manager consume the same profile patch format. Separate parsers can disagree about accepted rows or relative module paths, and rewriting a YAML document as plain objects loses comments and unresolved `!!js` expressions.
+
+## Decision
+
+`dsh-app-boot/patch-file` owns patch parsing and editing. The boot entry delegates to its parser; management operations use its comment-preserving YAML document and locked atomic writer. Relative plugin names resolve from the patch file's directory. Editing a row preserves unrelated fields, comments and expression text; deleting a row preserves its comments on a neighbour or the document.
+
+## Alternatives considered
+
+**Independent parsers or whole-document serialization.** Both let a file accepted by one consumer change meaning in another. A shared parser defines accepted input, while the YAML document editor retains syntax that plain object serialization discards.
+
+## Consequences
+
+Global row operations edit the profile user layer without modifying bundle files. A write holds the file lock across read, mutation and atomic replacement, then parses the written result. Unchanged documents are not rewritten.
+
+## Testing
+
+`packages/boot/app-boot/tests/patch-file.spec.ts` covers parsing, relative module paths, comments, `!!js`, row edits, locking and write failures. Plugin-manager tests exercise global row changes through real profile files.

+ 25 - 0
.agents/notes/implemented/architecture/2026-09-04-profile-patch-file-writer.zh.md

@@ -0,0 +1,25 @@
+# Agent Note: Profile patch 文件共用一个解析器与写入器
+
+Status: implemented
+
+[English](2026-09-04-profile-patch-file-writer.md) | 中文
+
+## Problem
+
+启动器与插件管理器消费相同的 profile patch 格式。独立解析器可能对可接受的行或相对模块路径得出不同结果,而将 YAML 文档转成普通对象重写会丢失注释和未求值的 `!!js` 表达式。
+
+## Decision
+
+`dsh-app-boot/patch-file` 负责 patch 解析与编辑。启动入口委托它解析;管理操作使用它保留注释的 YAML 文档和持锁的原子写入器。相对插件名以 patch 文件所在目录解析。编辑行时保留无关字段、注释与表达式文本;删除行时将其注释保留到相邻行或文档上。
+
+## Alternatives considered
+
+**独立解析器或整份文档序列化。** 两者都可能让一个消费方接受的文件在另一个消费方中改变含义。共享解析器定义可接受的输入,YAML 文档编辑器则保留普通对象序列化会丢弃的语法。
+
+## Consequences
+
+全局行操作编辑 profile 用户层,不修改组合包文件。一次写入在读取、修改和原子替换期间持有文件锁,然后解析写入结果。未改变的文档不会重写。
+
+## Testing
+
+`packages/boot/app-boot/tests/patch-file.spec.ts` 覆盖解析、相对模块路径、注释、`!!js`、行编辑、锁和写入失败。插件管理器测试通过真实 profile 文件验证全局行修改。

+ 0 - 6
.agents/notes/implemented/architecture/2026-09-04-settings-namespaces-resolve-per-scope.i18n.yaml

@@ -1,6 +0,0 @@
-# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
-# side as of the last confirmed-consistent state. Both languages carry equal authority;
-# after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-04-settings-namespaces-resolve-per-scope.md
-2026-09-04-settings-namespaces-resolve-per-scope.md: 1769b049080d307351b63ef9ea5c75fc4e6a70c9
-2026-09-04-settings-namespaces-resolve-per-scope.zh.md: 6cf041ae3e89083b2c931ab5c1342f63f1c70a39

+ 0 - 37
.agents/notes/implemented/architecture/2026-09-04-settings-namespaces-resolve-per-scope.md

@@ -1,37 +0,0 @@
-# Agent Note: A settings namespace resolves per scope
-
-Status: implemented
-
-English | [中文](2026-09-04-settings-namespaces-resolve-per-scope.zh.md)
-
-## Problem
-
-The settings seam registered a namespace once per process. That fit host rows, which exist once, and broke for agent presets, which mount the same plugin under several standing scopes: the second preset's `ctx.settings.register` threw "already registered" inside a `ctx.inject` continuation, the throw vanished into a nested fiber, and the second preset read the first's value. A person who wanted `skill-filesystem` to scan one extra root for the `research` preset only had no place to say so — the document had one section per namespace, and the plugin manager's per-preset configuration story needed one.
-
-## Decision
-
-**`dsh-scope` names scopes.** `createScope(ctx, key, { id })` records a stable name on the key, and `scopeIdOf(ctx)` answers the nearest named scope along a context's parent chain, so an agent's context resolves to the preset it joined. `dsh-agent-presets` names its standing scopes `preset/<id>`. Settings depends on `dsh-scope` alone and never on presets.
-
-**A namespace is a kind; a registration is an instance under a scope.** `register` reads the caller's scope through `scopeIdOf(this.ctx)` — the traceable service proxy binds `this.ctx` to the caller — and files the instance under it, the global scope being the absence of a name. Every registrant of a kind must carry the same schema envelope (`schema.toJSON()` compared with the seam's equality); a different one fails loud, as does a second instance under one scope. The first registrant's `validate` and `applies` are the kind's.
-
-**Four layers.** An instance resolves schema defaults, its own composition `base`, the document's global section, and its scope's section (`scopes.<id>.<ns>`) in that order, with the existing field-level merge. A global write re-resolves every instance of the kind, each gated on its own resolved value, so a scope that overrides the changed field is not disturbed; a scoped write commits that instance alone. Revisions are per section, kept off the registrations so a section written for a scope nothing has registered yet — a preset no session composed — is versioned like any other; such a write is accepted when the kind exists and is judged by the shared schema. `scopes` is a reserved namespace.
-
-**Describe per scope.** `describe()` answers one descriptor per kind under the global scope; `describe({ scope })` one per kind under a named scope, with `registered`, the scope's own `user` section, and `inherited` — the value without that section — so a surface can mark a field as overridden, inherited, or default. A scope no owner registered resolves over the global instance's composition base, because a schema may require what only a composition supplies, and a description never throws: layers the schema refuses describe as the layers beneath them, down to an empty section. The controller's `describe(scope?)` and its three write verbs take the same optional trailing `scope`, and both events carry the scope as a trailing argument that is absent for the global instance, which keeps every existing listener and the forwarded-event carrier unchanged.
-
-**`skill-filesystem` is the first consumer.** Its `customSkillDirs` resolves through `installSection` with the composition value as base; a change replaces the provider's roots and invalidates the catalog, so a person adds a root for one preset from the settings document while the process runs.
-
-## Alternatives considered
-
-**A per-preset settings document.** Rejected: the document is one file with one provider; a second file per preset would need its own watcher, lock, and reload path for the same format.
-
-**Keying instances by the preset id inside the settings seam.** Rejected: settings would then know presets; a named scope is the general form, and a future named scope — a workspace, a team — costs nothing.
-
-**One registration per kind with the scope passed on every read.** Rejected: a plugin reads its handle without knowing it runs in a preset; the instance's context already says where it is.
-
-## Consequences
-
-Two presets mounting one plugin keep separate values, and the silent collision is gone. A settings surface can offer "all presets" and "this preset" over one namespace. The global `describe()` still answers one row per kind, so the existing settings page renders unchanged while the plugin manager's page (next PR) reads per scope. A kind's `validate` judges every instance, and a scope-only instance appears in the global view as `registered: false`.
-
-## Testing
-
-`packages/settings/settings/tests/scopes.spec.ts` pins registration under a named and an inherited scope, kind-level schema agreement, duplicate and reserved refusals, scoped resolution, global-write fan-out with deep-equal gating, scoped writes through the handle and the provider, per-section revisions and conflicts, writes to an unregistered scope, external scope edits, malformed scope sections, and the scoped and global describers with redaction. `packages/settings/settings-file/tests/scopes.spec.ts` pins the `scopes.<id>.<ns>` layout in YAML and JSON with comments intact. `packages/api/settings-controller/tests/settings-controller.host.spec.ts` pins the scoped Remote reads and writes; `packages/core/scope/tests/scope.spec.ts` the named-scope resolution; `packages/preset/agent-presets/tests/overlay.spec.ts` the preset scope name; `packages/skill/skill-filesystem/tests/skill-filesystem.spec.ts` the live root change through settings.

+ 0 - 37
.agents/notes/implemented/architecture/2026-09-04-settings-namespaces-resolve-per-scope.zh.md

@@ -1,37 +0,0 @@
-# Agent Note:settings 命名空间按 scope 解析
-
-Status: implemented
-
-[English](2026-09-04-settings-namespaces-resolve-per-scope.md) | 中文
-
-## 问题
-
-settings seam 每个进程只允许注册一次命名空间。这对只存在一次的宿主行是对的,对 agent preset 却是坏的:同一个插件挂在多个常驻 scope 下,第二个 preset 的 `ctx.settings.register` 在 `ctx.inject` 延续里抛出"already registered",这个 throw 消失在嵌套 fiber 中,第二个 preset 读到的是第一个的值。想让 `skill-filesystem` 只为 `research` preset 多扫一个根目录的人无处可说——文档每个命名空间只有一段,而插件管理器的按 preset 配置需要这样一段。
-
-## 决定
-
-**`dsh-scope` 给 scope 起名。** `createScope(ctx, key, { id })` 在 key 上记录一个稳定的名字,`scopeIdOf(ctx)` 沿上下文的父链回答最近的有名 scope,于是 agent 的上下文解析到它加入的 preset。`dsh-agent-presets` 把常驻 scope 命名为 `preset/<id>`。settings 只依赖 `dsh-scope`,从不依赖 preset。
-
-**命名空间是一种 kind;注册是某个 scope 下的一个 instance。** `register` 经 `scopeIdOf(this.ctx)` 读取调用者的 scope——可追踪的服务代理把 `this.ctx` 绑到调用者——并把 instance 归档在其下,全局 scope 就是没有名字。同一 kind 的每个注册者必须带相同的 schema 信封(`schema.toJSON()` 用 seam 自己的相等谓词比较);不同的信封大声失败,同一 scope 下的第二个 instance 也是。第一个注册者的 `validate` 与 `applies` 属于 kind。
-
-**四层。** 一个 instance 按顺序解析 schema 默认值、自己的组合 `base`、文档的全局段、以及其 scope 的段(`scopes.<id>.<ns>`),沿用既有的字段级合并。全局写入重新解析该 kind 的每个 instance,各自按自身解析值门控,因此覆盖了被改字段的 scope 不会被打扰;scoped 写入只提交那个 instance。revision 按段记录,与注册分离,于是为尚无注册的 scope——还没有会话组合过的 preset——写入的段与其它段同样被版本化;只要 kind 存在,这样的写入就被接受并由共享 schema 判定。`scopes` 是保留的命名空间。
-
-**按 scope 描述。** `describe()` 在全局 scope 下每个 kind 回答一条描述符;`describe({ scope })` 在某个具名 scope 下每个 kind 回答一条,附带 `registered`、该 scope 自己的 `user` 段,以及 `inherited`——没有该段时的值——让界面能把字段标为已覆盖、继承或默认。没有注册者的 scope 在全局实例的组合 base 之上解析——schema 可能要求只有组合才提供的字段——而描述从不抛错:schema 拒绝的层按其下的层描述,直到空分区。controller 的 `describe(scope?)` 与三个写入动词接受同样的可选尾随 `scope`,两个事件都把 scope 作为尾随参数携带、全局 instance 时缺席,这让每个既有监听器与转发事件载体保持不变。
-
-**`skill-filesystem` 是第一个消费方。** 它的 `customSkillDirs` 以组合值为 base 经 `installSection` 解析;变化会替换 provider 的根目录并让目录失效,于是一个人可以在进程运行中从 settings 文档为某个 preset 添加一个根目录。
-
-## 考虑过的替代方案
-
-**每个 preset 一份 settings 文档。** 否决:文档是一个文件配一个 provider;每个 preset 再来一个文件,就要为同一格式再养一套 watcher、锁与重载路径。
-
-**在 settings seam 内以 preset id 作为 instance 的键。** 否决:settings 就此认识了 preset;具名 scope 是通用形式,将来的具名 scope——工作区、团队——不花任何代价。
-
-**每个 kind 只注册一次,每次读取传入 scope。** 否决:插件读取自己的句柄时并不知道自己跑在 preset 里;instance 的上下文已经说明它在哪。
-
-## 后果
-
-挂同一插件的两个 preset 各自保有值,静默碰撞消失。settings 界面可以在一个命名空间上提供"所有 preset"与"本 preset"。全局 `describe()` 仍然每个 kind 回答一行,因此既有设置页原样渲染,而插件管理器的页面(下一 PR)按 scope 读取。kind 的 `validate` 判定每个 instance,仅在 scope 下存在的 instance 在全局视图中呈现为 `registered: false`。
-
-## 测试
-
-`packages/settings/settings/tests/scopes.spec.ts` 钉住具名与继承 scope 下的注册、kind 级 schema 一致性、重复与保留名拒绝、scoped 解析、带 deep-equal 门控的全局写入扇出、经句柄与 provider 的 scoped 写入、按段的 revision 与冲突、对未注册 scope 的写入、外部 scope 编辑、畸形 scope 段,以及带脱敏的 scoped 与全局描述。`packages/settings/settings-file/tests/scopes.spec.ts` 钉住 YAML 与 JSON 中 `scopes.<id>.<ns>` 的布局且注释完好。`packages/api/settings-controller/tests/settings-controller.host.spec.ts` 钉住 scoped 的 Remote 读写;`packages/core/scope/tests/scope.spec.ts` 钉住具名 scope 解析;`packages/preset/agent-presets/tests/overlay.spec.ts` 钉住 preset 的 scope 名;`packages/skill/skill-filesystem/tests/skill-filesystem.spec.ts` 钉住经 settings 的根目录在线变更。

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-09-plugin-management-in-the-web-sidebar.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-09-plugin-management-in-the-web-sidebar.md
-2026-09-09-plugin-management-in-the-web-sidebar.md: 9dd717ce757fb606a2454732c90e239c86ff40fa
-2026-09-09-plugin-management-in-the-web-sidebar.zh.md: 90ae509e7815d7eb63b9e48aa0ad239862ac1a77
+2026-09-09-plugin-management-in-the-web-sidebar.md: 56c057e3d353efd9b6983a7883dad88d908aea22
+2026-09-09-plugin-management-in-the-web-sidebar.zh.md: 7b927db0a0ad231a5adbc2feda818bc8ae774be3

+ 3 - 3
.agents/notes/implemented/architecture/2026-09-09-plugin-management-in-the-web-sidebar.md

@@ -6,13 +6,13 @@ English | [中文](2026-09-09-plugin-management-in-the-web-sidebar.zh.md)
 
 ## Problem
 
-[Plugin management in Web settings](2026-09-04-plugin-management-in-web-settings.md) put the manager on a tab of the Settings dialog's Plugins section, beside the configuration tab. Settings is a modal over the current Session, while a person managing what is installed has no Session in mind; the dialog's width and its one-section-at-a-time shell also leave no room for a package page, an install run, and the list side by side. The layout has since gained root-scoped main panels behind sidebar entries ([global main panels](2026-09-08-global-main-panels.md)), which is the lifetime and the room the manager needs.
+Installed packages belong to the running profile, while Settings is a modal over a Session. The management page needs room for package details and installation output. The layout's [global main panels](2026-09-08-global-main-panels.md) provide that lifetime and space.
 
 ## Decision
 
-**Management is a sidebar entry; configuration stays in Settings.** `ui-plugin-manager` registers a `sidebar.panellist` entry and the `main` panel it opens under one id, `plugins`: the sidebar renders the localized **Plugins** label, this package the icon and the page. The page is root-scoped and bound to no Session; it carries its own heading, intro, refresh, and **Add plugin**, and scrolls inside the main column. What the page shows did not change: the packs and plugins, each package's page, the install dialog, and the confirmations are the former tab's. The Settings **Plugins** section keeps plugin configuration, and `ui-settings-plugins` renders a single `settings.plugins.tab` contribution as the page itself, without a tab strip, so the section reads as the configuration page it now is. The preset detail page in Settings keeps its **Capabilities** section over the same store.
+**Management is a sidebar entry; configuration stays in Settings.** `ui-plugin-manager` registers a `sidebar.panellist` entry and the `main` panel it opens under `plugins`. The page manages packages and global rows, displays install output and confirms dependency-sensitive actions. The Settings Plugins section keeps the global configuration cards and renders its single tab without a tab strip.
 
-**Configuration does not move.** A plugin's configuration binds a settings namespace under a scope — the global instance or one preset's — and the Settings shell owns that scope machinery, one selection serving the section and every preset's detail page. Moving the cards onto the management page would duplicate the shell's scope selection and split settings across two entry points, while a person looks for a value in Settings. The plugin's own page will instead point at its configurable sections once Settings can be opened on one section, which needs only a deep link.
+**One store follows Host state.** The manager controller combines package views with global inventory, refreshes after management operations and reconnects, and keeps installation progress under the owning job. Configuration cards use the existing global settings bindings.
 
 ## Alternatives considered
 

+ 3 - 3
.agents/notes/implemented/architecture/2026-09-09-plugin-management-in-the-web-sidebar.zh.md

@@ -6,13 +6,13 @@ Status: implemented
 
 ## 问题
 
-[插件管理进入 Web 设置](2026-09-04-plugin-management-in-web-settings.zh.md)把管理器放在设置对话框「插件」分区的一个标签页上,与配置标签页并列。设置是盖在当前 Session 之上的模态框,而一个在管理已安装内容的人心里并没有 Session;对话框的宽度和一次只显示一个分区的外壳,也放不下包页面、安装运行与列表并排。布局此后有了侧栏入口背后的根级主面板([全局主面板](2026-09-08-global-main-panels.zh.md)),这正是管理器需要的生命周期与空间。
+已安装的包属于运行中的 profile,而设置是覆盖在 Session 上的弹窗。管理页面需要容纳包详情与安装输出。布局的[全局主面板](2026-09-08-global-main-panels.zh.md)提供对应的生命周期与空间。
 
 ## 决定
 
-**管理是侧栏入口;配置留在设置里。** `ui-plugin-manager` 用同一个 id `plugins` 注册一个 `sidebar.panellist` 条目和它打开的 `main` 面板:侧栏渲染本地化的**插件**标签,本包提供图标与页面。页面是根级作用域,不绑定任何 Session;它带自己的标题、简介、刷新与**添加插件**,并在主列内部滚动。页面显示的内容没有变:插件包与插件、每个包的页面、安装对话框与各个确认框都来自原来的标签页。设置的**插件**分区保留插件配置,`ui-settings-plugins` 把唯一的 `settings.plugins.tab` 贡献直接渲染成这一页,没有标签条,因此该分区读起来就是它现在的样子——配置页。设置里的预设详情页保留读同一个 store 的**能力**段。
+**管理位于侧栏,配置保留在设置中。** `ui-plugin-manager` 在 `plugins` 下注册 `sidebar.panellist` 入口与它打开的 `main` 面板。页面管理包与全局行、展示安装输出,并确认影响依赖的操作。设置的插件分区保留全局配置卡片,单个标签页直接展示内容,不显示标签栏。
 
-**配置不搬。** 插件的配置把一个 settings 命名空间绑定在某个 scope 下——全局实例或某个预设——而设置外壳拥有这套 scope 机制,一处选择同时服务分区与每个预设的详情页。把卡片搬到管理页会复制外壳的 scope 选择,并把设置拆到两个入口,而人找一个值时会去设置里找。等设置能按分区打开后,插件自己的页面会转而指向它的可配置分区,那只需要一个深链接。
+**一个 store 跟随 Host 状态。** 管理器控制器结合包视图与全局 inventory,在管理操作和重连后刷新,并按所属 job 保存安装进度。配置卡片使用原有全局 settings 绑定。
 
 ## 考虑过的替代方案
 

+ 0 - 15
apps/web/tests/expected/agent-preset-authoring/created.expected.md

@@ -31,9 +31,6 @@
       - 'button "新任务默认: 标准模式" [disabled] [pressed]':
         - text: 标准模式 内置 新任务默认 功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。
         - code: standard
-      - 'button "配置: 标准模式"':
-        - img
-        - text: 配置
       - 'button "查看: 标准模式"':
         - img
         - text: 查看
@@ -44,9 +41,6 @@
       - 'button "设为默认: PTC 模式"':
         - text: PTC 模式 内置 功能完整的编码 Agent,但默认不提供 workflow 工具;其他工具通过 PTC 模式 SDK 呈现,让模型用一个 TypeScript 程序组合多步操作。
         - code: ptc
-      - 'button "配置: PTC 模式"':
-        - img
-        - text: 配置
       - 'button "查看: PTC 模式"':
         - img
         - text: 查看
@@ -57,9 +51,6 @@
       - 'button "设为默认: 极简模式"':
         - text: 极简模式 内置 仅提供持久 shell 的单工具编码 Agent。
         - code: minimal
-      - 'button "配置: 极简模式"':
-        - img
-        - text: 配置
       - 'button "查看: 极简模式"':
         - img
         - text: 查看
@@ -70,9 +61,6 @@
       - 'button "设为默认: 创造模式"':
         - text: 创造模式 内置 用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导。
         - code: cordis
-      - 'button "配置: 创造模式"':
-        - img
-        - text: 配置
       - 'button "查看: 创造模式"':
         - img
         - text: 查看
@@ -85,9 +73,6 @@
       - 'button "设为默认: 我的模式"':
         - text: 我的模式 自定义 仅提供持久 shell 的单工具编码 Agent。
         - code: my-agent
-      - 'button "配置: 我的模式"':
-        - img
-        - text: 配置
       - 'button "查看路径: 我的模式"':
         - img
         - text: 查看路径

+ 0 - 18
apps/web/tests/expected/agent-preset-authoring/damaged.expected.md

@@ -31,9 +31,6 @@
       - 'button "新任务默认: 标准模式" [disabled] [pressed]':
         - text: 标准模式 内置 新任务默认 功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。
         - code: standard
-      - 'button "配置: 标准模式"':
-        - img
-        - text: 配置
       - 'button "查看: 标准模式"':
         - img
         - text: 查看
@@ -44,9 +41,6 @@
       - 'button "设为默认: PTC 模式"':
         - text: PTC 模式 内置 功能完整的编码 Agent,但默认不提供 workflow 工具;其他工具通过 PTC 模式 SDK 呈现,让模型用一个 TypeScript 程序组合多步操作。
         - code: ptc
-      - 'button "配置: PTC 模式"':
-        - img
-        - text: 配置
       - 'button "查看: PTC 模式"':
         - img
         - text: 查看
@@ -57,9 +51,6 @@
       - 'button "设为默认: 极简模式"':
         - text: 极简模式 内置 仅提供持久 shell 的单工具编码 Agent。
         - code: minimal
-      - 'button "配置: 极简模式"':
-        - img
-        - text: 配置
       - 'button "查看: 极简模式"':
         - img
         - text: 查看
@@ -70,9 +61,6 @@
       - 'button "设为默认: 创造模式"':
         - text: 创造模式 内置 用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导。
         - code: cordis
-      - 'button "配置: 创造模式"':
-        - img
-        - text: 配置
       - 'button "查看: 创造模式"':
         - img
         - text: 查看
@@ -86,9 +74,6 @@
         - text: broken-yaml 加载失败 自定义 暂无描述。
         - alert: "the composition is not valid YAML: unexpected end of the stream within a flow collection (3:1)"
         - code: broken-yaml
-      - 'button "配置: broken-yaml" [disabled]':
-        - img
-        - text: 配置
       - 'button "查看路径: broken-yaml"':
         - img
         - text: 查看路径
@@ -103,9 +88,6 @@
         - text: 幽灵预设 加载失败 自定义 composition 已被手动删除。
         - alert: the composition file agent.cordis.yml is missing — the directory still occupies the id; delete it or restore the file
         - code: ghost
-      - 'button "配置: 幽灵预设" [disabled]':
-        - img
-        - text: 配置
       - 'button "查看路径: 幽灵预设"':
         - img
         - text: 查看路径

+ 0 - 12
apps/web/tests/expected/agent-preset-authoring/section.expected.md

@@ -31,9 +31,6 @@
       - 'button "新任务默认: 标准模式" [disabled] [pressed]':
         - text: 标准模式 内置 新任务默认 功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。
         - code: standard
-      - 'button "配置: 标准模式"':
-        - img
-        - text: 配置
       - 'button "查看: 标准模式"':
         - img
         - text: 查看
@@ -44,9 +41,6 @@
       - 'button "设为默认: PTC 模式"':
         - text: PTC 模式 内置 功能完整的编码 Agent,但默认不提供 workflow 工具;其他工具通过 PTC 模式 SDK 呈现,让模型用一个 TypeScript 程序组合多步操作。
         - code: ptc
-      - 'button "配置: PTC 模式"':
-        - img
-        - text: 配置
       - 'button "查看: PTC 模式"':
         - img
         - text: 查看
@@ -57,9 +51,6 @@
       - 'button "设为默认: 极简模式"':
         - text: 极简模式 内置 仅提供持久 shell 的单工具编码 Agent。
         - code: minimal
-      - 'button "配置: 极简模式"':
-        - img
-        - text: 配置
       - 'button "查看: 极简模式"':
         - img
         - text: 查看
@@ -70,9 +61,6 @@
       - 'button "设为默认: 创造模式"':
         - text: 创造模式 内置 用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导。
         - code: cordis
-      - 'button "配置: 创造模式"':
-        - img
-        - text: 配置
       - 'button "查看: 创造模式"':
         - img
         - text: 查看

+ 1 - 1
apps/web/tests/expected/plugin-manager/manager.expected.md

@@ -15,4 +15,4 @@
   - listitem:
     - button "查看 示例插件": 示例插件
     - text: "Web e2e fixture: a plain plugin module with no bundle, added to a composition per row."
-    - button "加入到…"
+    - button "加入全局"

+ 0 - 116
apps/web/tests/expected/plugin-manager/preset-detail.expected.md

@@ -1,116 +0,0 @@
-- button "返回 Agent 预设":
-  - img
-  - text: Agent 预设 标准模式
-- heading "标准模式" [level=2]
-- text: 内置 新任务默认
-- paragraph: 功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。
-- heading "能力" [level=3]
-- button "给 标准模式 添加能力": 添加
-- list:
-  - listitem:
-    - text: 人设 助手的身份与语气
-    - switch "启用 人设" [checked]
-  - listitem:
-    - text: 项目指令 读取 AGENTS.md 等项目说明
-    - switch "启用 项目指令" [checked]
-  - listitem:
-    - text: 终端 运行 shell 命令
-    - switch "启用 终端" [checked]
-  - listitem:
-    - text: PowerShell 终端 在 Windows 上运行 PowerShell
-    - switch "启用 PowerShell 终端" [disabled]
-  - listitem:
-    - text: 文件读写 读取、创建和修改文件
-    - switch "启用 文件读写" [checked]
-  - listitem:
-    - text: 文件搜索 按名称和内容查找文件
-    - switch "启用 文件搜索" [checked]
-  - listitem:
-    - text: 后台任务 启动和查看长时间运行的命令
-    - switch "启用 后台任务" [checked]
-  - listitem:
-    - text: 技能 从本地目录加载技能
-    - switch "启用 技能" [checked]
-  - listitem:
-    - text: 技能调用 让助手调用技能
-    - switch "启用 技能调用" [checked]
-  - listitem:
-    - text: 目标命令 /goal 命令
-    - switch "启用 目标命令" [checked]
-  - listitem:
-    - text: 目标 记录并跟踪当前目标
-    - switch "启用 目标" [checked]
-  - listitem:
-    - text: 计划模式 先规划再动手
-    - switch "启用 计划模式" [checked]
-  - listitem:
-    - text: 上下文压缩 对话过长时自动整理
-    - switch "启用 上下文压缩" [checked]
-  - listitem:
-    - text: 压缩命令 /compact 命令
-    - switch "启用 压缩命令" [checked]
-  - listitem:
-    - text: 工具结果裁剪 丢弃过时的工具输出
-    - switch "启用 工具结果裁剪" [checked]
-  - listitem:
-    - text: 子代理控制 查看和管理子代理
-    - switch "启用 子代理控制" [checked]
-  - listitem:
-    - text: 子代理列表 列出正在运行的子代理
-    - switch "启用 子代理列表" [checked]
-  - listitem:
-    - text: 子代理 把任务分派给并行的助手
-    - switch "启用 子代理" [checked]
-  - listitem:
-    - text: 分支子代理 复制当前上下文继续任务
-    - switch "启用 分支子代理" [checked]
-  - listitem:
-    - text: Codex 子代理 把任务交给 Codex 执行
-    - switch "启用 Codex 子代理" [disabled]
-  - listitem:
-    - text: Claude Code 子代理 把任务交给 Claude Code 执行
-    - switch "启用 Claude Code 子代理" [disabled]
-  - listitem:
-    - text: 工作流引擎 运行多步骤工作流
-    - switch "启用 工作流引擎" [checked]
-  - listitem:
-    - text: 工作流 编排多步骤任务
-    - switch "启用 工作流" [checked]
-  - listitem:
-    - text: Ralph 循环 反复执行直到完成
-    - switch "启用 Ralph 循环" [disabled]
-  - listitem:
-    - text: 向用户提问 需要时向你确认
-    - switch "启用 向用户提问" [checked]
-  - listitem:
-    - text: 待办 维护任务清单
-    - switch "启用 待办" [checked]
-  - listitem:
-    - text: 网页 搜索和抓取网页
-    - switch "启用 网页" [checked]
-  - listitem:
-    - text: tool-present @deepseek-ai/dsh-tool-present
-    - switch "启用 tool-present" [checked]
-  - listitem:
-    - text: "示例插件 本地 Web e2e fixture: a plain plugin module with no bundle, added to a composition per row."
-    - button "从这个预设删除 示例插件":
-      - img
-    - switch "启用 示例插件" [checked]
-- heading "设置" [level=3]
-- list:
-  - listitem:
-    - 'button "展开设置: 终端"':
-      - text: 终端 限制 agent 运行的每一条命令。
-      - img
-  - listitem:
-    - 'button "展开设置: Agent 循环"':
-      - text: Agent 循环 Agent 如何派发工具调用。
-      - img
-  - listitem:
-    - 'button "展开设置: Subagent"':
-      - text: Subagent 控制 Agent 为 Subagent 选择模型的权限。
-      - img
-  - listitem:
-    - 'button "展开设置: 网页搜索"':
-      - text: 网页搜索 DeepSeek 搜索提供方。
-      - img

+ 2 - 4
apps/web/tests/fixtures/assembled-remote.fixture.json

@@ -49,11 +49,9 @@
               "set": false
             }
           ],
-          "revision": 0,
-          "registered": true
+          "revision": 0
         }
-      ],
-      "scopes": []
+      ]
     }
   },
   "credentialsDescribe": {

+ 10 - 105
apps/web/tests/plugin-manager.e2e.ts

@@ -1,11 +1,9 @@
 // Web e2e scenario: the plugin manager page behind the sidebar's Plugins entry over a scaffold
 // profile runtime — one installed bundle switched on, one plain plugin added to
-// an agent preset and switched off there, and the configuration tab writing one
-// field under that preset's settings scope. Zero model calls: everything is
+// the global user layer. Zero model calls: everything is
 // client state plus the profile files and the settings document, so there is
 // no fixture and a stray stream would fail loud on the open llm seam.
-import { mkdtemp, readFile, rm } from 'node:fs/promises'
-import { tmpdir } from 'node:os'
+import { readFile } from 'node:fs/promises'
 import { fileURLToPath } from 'node:url'
 import type { Browser, Page } from 'playwright'
 import { chromium } from 'playwright'
@@ -19,7 +17,6 @@ import { ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts'
 
 const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/plugin-manager', import.meta.url))
 const MANAGER_EXPECTED = join(SNAPSHOT_DIR, 'manager.expected.md')
-const DETAIL_EXPECTED = join(SNAPSHOT_DIR, 'preset-detail.expected.md')
 const LIVE_EXPECTED = join(SNAPSHOT_DIR, 'live-enabled.expected.md')
 const FIXTURE_PLUGINS = fileURLToPath(new URL('./fixtures/plugins', import.meta.url))
 const MODE = webSnapshotMode()
@@ -29,15 +26,9 @@ describe('web e2e: plugin manager', () => {
   let browser: Browser
   let page: Page
   let tripwire: ReturnType<typeof watchConsole>
-  /** The writable preset root the scenario's preset overlays land in. */
-  let presetRoot: string
 
   beforeAll(async () => {
-    presetRoot = await mkdtemp(join(tmpdir(), 'dsh-web-e2e-presets-'))
     scaffold = await launchWebScaffold({
-      // The scaffold pins a roster without a user root, so a preset's user
-      // patch layer needs one: an empty root beside the shipped presets.
-      agentPresets: { roots: [{ path: presetRoot, trust: 'user' }], default: 'standard' },
       profileRuntime: {
         packages: [
           { dir: join(FIXTURE_PLUGINS, 'fixture-bundle') },
@@ -55,7 +46,6 @@ describe('web e2e: plugin manager', () => {
   afterAll(async () => {
     await browser?.close()
     await scaffold?.close()
-    await rm(presetRoot, { recursive: true, force: true })
   })
 
   /** Close any open settings dialog, so the sidebar and the main column are clickable. */
@@ -75,29 +65,6 @@ describe('web e2e: plugin manager', () => {
     return panel
   }
 
-  /** Open the settings dialog on the Plugins section: the configuration page. */
-  async function openPluginsSettings() {
-    await closeSettings()
-    await page.getByRole('button', { name: '设置', exact: true }).click()
-    const dialog = page.getByRole('dialog', { name: '设置' })
-    await dialog.waitFor({ timeout: 10_000 })
-    await dialog.getByRole('button', { name: '插件', exact: true }).click()
-    await dialog.getByRole('heading', { name: '插件', exact: true }).waitFor({ timeout: 10_000 })
-    return dialog
-  }
-
-  /** Open the settings dialog on one preset's detail page. */
-  async function openPresetDetail(name: string) {
-    await closeSettings()
-    await page.getByRole('button', { name: '设置', exact: true }).click()
-    const dialog = page.getByRole('dialog', { name: '设置' })
-    await dialog.waitFor({ timeout: 10_000 })
-    await dialog.getByRole('button', { name: 'Agent 预设', exact: true }).click()
-    await dialog.getByRole('button', { name: `配置: ${name}` }).click()
-    await dialog.getByRole('heading', { name }).waitFor({ timeout: 10_000 })
-    return dialog
-  }
-
   /** One file under the harness home, or the empty string while it does not exist. */
   async function homeFile(...segments: string[]): Promise<string> {
     return readFile(join(scaffold.harnessHome, ...segments), 'utf8').catch(() => '')
@@ -110,12 +77,12 @@ describe('web e2e: plugin manager', () => {
     await panel.getByText('示例组合包', { exact: true }).waitFor({ timeout: 20_000 })
     expect(await panel.getByText('示例插件', { exact: true }).count()).toBe(1)
     // The bundle is installed but not enabled; the plain plugin carries no
-    // switch, only its **Add to…** menu; uninstall waits on the plugin's page,
+    // switch, only its global add button; uninstall waits on the plugin's page,
     // which names the module and where it is composed, and the crumb leads back.
     const toggle = panel.getByRole('switch', { name: '启用 示例组合包' })
     expect(await toggle.getAttribute('aria-checked')).toBe('false')
     expect(await panel.getByRole('switch', { name: '启用 示例插件' }).count()).toBe(0)
-    expect(await panel.getByRole('button', { name: '加入到…' }).count()).toBe(1)
+    expect(await panel.getByRole('button', { name: '加入全局' }).count()).toBe(1)
     expect(await panel.getByRole('button', { name: '卸载 示例插件' }).count()).toBe(0)
     await panel.getByRole('button', { name: '查看 示例插件' }).click()
     await panel.getByRole('button', { name: '卸载 示例插件' }).waitFor({ timeout: 5_000 })
@@ -156,79 +123,17 @@ describe('web e2e: plugin manager', () => {
     expect(tripwire.pageErrors).toEqual([])
   }, 60_000)
 
-  it('adds a plain plugin to a preset, then switches it off and deletes it on the preset\'s detail page', async () => {
-    onTestFailed(() => saveFailureShot(page, 'web-e2e-plugin-manager-preset-row'))
+  it('adds a declared plugin to the global user layer', async () => {
     const panel = await openPluginsPanel()
-    await panel.getByRole('button', { name: '加入到…' }).click()
-    await page.getByRole('menuitem', { name: '预设:标准模式' }).click()
-
-    const overlay = (): Promise<string> => readFile(join(presetRoot, 'standard', 'cordis.patch.yml'), 'utf8').catch(() => '')
-    await expect.poll(async () => (await overlay()).includes('@fixture/plain-plugin'), { timeout: 10_000 }).toBe(true)
-    expect(await overlay()).toContain('id: fixture/plain-plugin')
-    // The **Add to…** menu now marks that preset as joined; the menu closes
-    // through its anchor.
-    await panel.getByRole('button', { name: '加入到…' }).click()
-    await page.getByRole('menuitem', { name: '预设:标准模式(已加入)' }).waitFor({ timeout: 5_000 })
-    await panel.getByRole('button', { name: '加入到…' }).click()
-    await expect.poll(() => page.getByRole('menuitem').count(), { timeout: 5_000 }).toBe(0)
-
-    // The preset's detail page, behind the gear on its card, lists the new
-    // row under the package's title as the user's, switched on, with the
-    // local mark.
-    const dialog = await openPresetDetail('标准模式')
-    const presetRow = dialog.locator('[data-preset-row="fixture/plain-plugin"]')
-    const rowToggle = presetRow.getByRole('switch', { name: '启用 示例插件' })
-    await rowToggle.waitFor({ timeout: 10_000 })
-    expect(await rowToggle.getAttribute('aria-checked')).toBe('true')
-    expect(await presetRow.getAttribute('data-plugin-source')).toBe('user')
-    expect(await presetRow.getByText('本地', { exact: true }).count()).toBe(1)
-    const settingsSection = dialog.locator('[data-settings-scope="preset/standard"]')
-    await expect.poll(() => settingsSection.getByRole('button', { name: /^展开设置:/ }).count(), { timeout: 10_000 }).toBe(4)
-    const detailSnapshot = await captureStableAria(page, '[data-preset-detail="standard"]', scaffold.workspaceCwd)
-    await compareOrRefreshGolden(DETAIL_EXPECTED, detailSnapshot, MODE)
-
-    await rowToggle.click()
-    await expect.poll(async () => (await overlay()).includes('disabled: true'), { timeout: 10_000 }).toBe(true)
-    await expect.poll(() => rowToggle.getAttribute('aria-checked'), { timeout: 10_000 }).toBe('false')
-
-    await presetRow.getByRole('button', { name: '从这个预设删除 示例插件' }).click()
-    await expect.poll(async () => (await overlay()).includes('@fixture/plain-plugin'), { timeout: 10_000 }).toBe(false)
-    await expect.poll(() => rowToggle.count(), { timeout: 10_000 }).toBe(0)
-    // The breadcrumb leads back to the roster.
-    await dialog.getByRole('button', { name: '返回 Agent 预设' }).click()
-    await dialog.getByRole('button', { name: '配置: 标准模式' }).waitFor({ timeout: 10_000 })
-    expect(tripwire.pageErrors).toEqual([])
-  }, 60_000)
-
-  it('writes a configuration field under one preset scope and leaves the shared value alone', async () => {
-    onTestFailed(() => saveFailureShot(page, 'web-e2e-plugin-manager-scope'))
-    const dialog = await openPresetDetail('标准模式')
-    // The detail page's settings section edits the preset's own scope.
-    const settingsSection = dialog.locator('[data-settings-scope="preset/standard"]')
-    await settingsSection.getByRole('button', { name: '展开设置: 终端' }).click()
-    const timeout = settingsSection.getByLabel('命令超时(毫秒)')
-    await timeout.waitFor({ timeout: 10_000 })
-    expect(await timeout.inputValue()).toBe('60000')
-    await timeout.fill('12000')
-    await settingsSection.getByRole('button', { name: '保存', exact: true }).click()
-
-    const settings = (): Promise<string> => homeFile('settings.yaml')
-    await expect.poll(async () => (await settings()).includes('timeoutMs: 12000'), { timeout: 10_000 }).toBe(true)
-    const document = await settings()
-    expect(document).toContain('scopes:')
-    expect(document).toContain('preset/standard:')
-    expect(document.indexOf('scopes:')).toBeLessThan(document.indexOf('timeoutMs: 12000'))
-    // On the configuration tab the shared instance still shows the composed default.
-    const plugins = await openPluginsSettings()
-    await plugins.getByRole('button', { name: '展开设置: 终端' }).click()
-    await expect.poll(() => plugins.getByLabel('命令超时(毫秒)').inputValue(), { timeout: 5_000 }).toBe('60000')
-    expect(await plugins.getByText('已覆盖').count()).toBe(0)
+    const card = panel.locator('[data-plugin-package="@fixture/plain-plugin"]')
+    await card.getByRole('button', { name: '加入全局', exact: true }).click()
+    await expect.poll(() => homeFile('profiles', 'scaffold', 'cordis.patch.yml')).toContain('@fixture/plain-plugin')
     expect(tripwire.pageErrors).toEqual([])
-  }, 60_000)
+  })
 
   it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
     expect(tripwire.warnings).toEqual([])
-    await assertFixtureInventory(SNAPSHOT_DIR, ['manager.expected.md', 'preset-detail.expected.md', 'live-enabled.expected.md'])
+    await assertFixtureInventory(SNAPSHOT_DIR, ['manager.expected.md', 'live-enabled.expected.md'])
   })
 })
 

+ 2 - 2
docs/config-catalog.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/config-catalog.md
-config-catalog.md: 71e7e473c21b07a622cdd26315deb19dc1d56641
-config-catalog.zh.md: d127165e2291b8f0dade293ae1f484f9a147d1df
+config-catalog.md: 6f9e1de0be374f5e508223479e0e5fb7f02d4e25
+config-catalog.zh.md: b5164c6010ee2da8c8bcd9d0affe6214b850ab41

+ 3 - 3
docs/config-catalog.md

@@ -157,7 +157,7 @@ export interface PresetRoot {
 export type PresetTrust = 'system' | 'user'
 ```
 
-Source: [`packages/preset/agent-presets/src/preset.ts:60`](../packages/preset/agent-presets/src/preset.ts)
+Source: [`packages/preset/agent-presets/src/preset.ts:52`](../packages/preset/agent-presets/src/preset.ts)
 
 <a id="deepseek-aidsh-agent-tool-presentation"></a>
 
@@ -942,7 +942,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/host/plugin-manager/src/index.ts:42`](../packages/host/plugin-manager/src/index.ts)
+Source: [`packages/host/plugin-manager/src/index.ts:40`](../packages/host/plugin-manager/src/index.ts)
 
 <a id="deepseek-aidsh-host-webserver"></a>
 
@@ -2259,7 +2259,7 @@ export interface Config {
 }
 ```
 
-Source: [`packages/skill/skill-filesystem/src/index.ts:68`](../packages/skill/skill-filesystem/src/index.ts)
+Source: [`packages/skill/skill-filesystem/src/index.ts:49`](../packages/skill/skill-filesystem/src/index.ts)
 
 <a id="deepseek-aidsh-spill-local"></a>
 

+ 3 - 3
docs/config-catalog.zh.md

@@ -159,7 +159,7 @@ export interface PresetRoot {
 export type PresetTrust = 'system' | 'user'
 ```
 
-来源:[`packages/preset/agent-presets/src/preset.ts:60`](../packages/preset/agent-presets/src/preset.ts)
+来源:[`packages/preset/agent-presets/src/preset.ts:52`](../packages/preset/agent-presets/src/preset.ts)
 
 <a id="deepseek-aidsh-agent-tool-presentation"></a>
 
@@ -944,7 +944,7 @@ export interface Config {
 }
 ```
 
-来源:[`packages/host/plugin-manager/src/index.ts:42`](../packages/host/plugin-manager/src/index.ts)
+来源:[`packages/host/plugin-manager/src/index.ts:40`](../packages/host/plugin-manager/src/index.ts)
 
 
 <a id="deepseek-aidsh-host-webserver"></a>
@@ -2262,7 +2262,7 @@ export interface Config {
 }
 ```
 
-来源:[`packages/skill/skill-filesystem/src/index.ts:68`](../packages/skill/skill-filesystem/src/index.ts)
+来源:[`packages/skill/skill-filesystem/src/index.ts:49`](../packages/skill/skill-filesystem/src/index.ts)
 
 <a id="deepseek-aidsh-spill-local"></a>
 

+ 2 - 2
docs/event-producer-consumer.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/event-producer-consumer.md
-event-producer-consumer.md: 59f96469fda814d7bbc9dad5e979a16595e57043
-event-producer-consumer.zh.md: 76d47f2114bc0fbcc591732268669840945a9bca
+event-producer-consumer.md: f30ae258881eee0cf3b220aaf760e17ac1a900dd
+event-producer-consumer.zh.md: 02645d1b5ba78a23f75a3d4c7e81fea2c2809f3a

+ 4 - 4
docs/event-producer-consumer.md

@@ -48,15 +48,15 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` |
 | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:72`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
 | `permission-presets/catalog-changed` | `emit` | [`packages/interaction/permission-presets/src/types.ts:44`](../packages/interaction/permission-presets/src/types.ts) | [`permission-presets`](../packages/interaction/permission-presets) (`events.dispatch`) | `remotes` |
-| `plugins/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:240`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
-| `plugins/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:246`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
+| `plugins/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:233`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
+| `plugins/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:239`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
 | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
 | `session/created` | `emit` | [`packages/core/session/src/index.ts:50`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
 | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:60`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
 | `session/event` | `emit` | [`packages/core/session/src/index.ts:72`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `agent-team`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
 | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:81`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) |
-| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:137`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `remotes` |
-| `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:123`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
+| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:105`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `remotes` |
+| `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:92`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
 | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:296`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
 | `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:170`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server`, [`subagent`](../packages/subagent/subagent) |
 | `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:144`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |

+ 2 - 2
docs/event-producer-consumer.zh.md

@@ -57,8 +57,8 @@
 | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:60`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
 | `session/event` | `emit` | [`packages/core/session/src/index.ts:72`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `agent-team`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
 | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:81`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) |
-| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:137`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `remotes` |
-| `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:123`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
+| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:105`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `remotes` |
+| `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:92`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
 | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:296`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
 | `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:170`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server`, [`subagent`](../packages/subagent/subagent) |
 | `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:144`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |

+ 2 - 2
docs/module-graph.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/module-graph.md
-module-graph.md: 7785652ebf94ab424844bf28317a26676d891d88
-module-graph.zh.md: 87236d69fb4099d2b73ec9cf10791500cc1cc3ba
+module-graph.md: 45cc05fb30f1c0842bd37e310267cf0bf4d12729
+module-graph.zh.md: c1c33021d1aaaaf7688f08cff67325078bca151a

+ 10 - 15
docs/module-graph.md

@@ -461,7 +461,6 @@ flowchart TD
   pkg_session_projection --> pkg_session
   pkg_settings --> pkg_brand
   pkg_settings --> pkg_invariants
-  pkg_settings --> pkg_scope
   pkg_settings --> pkg_session
   pkg_session_snapshot --> pkg_http_proxy
   pkg_session_snapshot --> pkg_session
@@ -551,7 +550,6 @@ flowchart TD
   pkg_fs_observation_policy --> pkg_fs
   pkg_skill_filesystem --> pkg_fs
   pkg_skill_filesystem --> pkg_home_paths
-  pkg_skill_filesystem --> pkg_settings
   pkg_skill_filesystem --> pkg_skill
   pkg_web_search_deepseek --> pkg_agent
   pkg_web_search_deepseek --> pkg_credentials
@@ -582,6 +580,11 @@ flowchart TD
   pkg_experimental_ptc_runtime_python --> pkg_ptc_runtime
   pkg_experimental_ptc_runtime_python --> pkg_timeout
   pkg_experimental_ptc_runtime_python --> pkg_util_values
+  pkg_host_plugin_manager --> pkg_agent
+  pkg_host_plugin_manager --> pkg_app_boot
+  pkg_host_plugin_manager --> pkg_plugin_manager
+  pkg_host_plugin_manager --> pkg_typert_protocol
+  pkg_host_plugin_manager --> pkg_util_values
   pkg_commands --> pkg_agent
   pkg_commands --> pkg_attachment
   pkg_commands --> pkg_brand
@@ -850,7 +853,6 @@ flowchart TD
   pkg_mcp_resources --> pkg_system_prompt
   pkg_mcp_resources --> pkg_tools
   pkg_agent_presets --> pkg_agent
-  pkg_agent_presets --> pkg_app_boot
   pkg_agent_presets --> pkg_atomic_write
   pkg_agent_presets --> pkg_home_paths
   pkg_agent_presets --> pkg_invariants
@@ -936,7 +938,6 @@ flowchart TD
   pkg_api_settings_controller --> pkg_agent_presets
   pkg_api_settings_controller --> pkg_credentials
   pkg_api_settings_controller --> pkg_native_command
-  pkg_api_settings_controller --> pkg_scope
   pkg_api_settings_controller --> pkg_session
   pkg_api_settings_controller --> pkg_settings
   pkg_api_settings_controller --> pkg_typert_protocol
@@ -963,12 +964,6 @@ flowchart TD
   pkg_host_plugin_inventory --> pkg_app_boot
   pkg_host_plugin_inventory --> pkg_brand
   pkg_host_plugin_inventory --> pkg_typert_protocol
-  pkg_host_plugin_manager --> pkg_agent
-  pkg_host_plugin_manager --> pkg_agent_presets
-  pkg_host_plugin_manager --> pkg_app_boot
-  pkg_host_plugin_manager --> pkg_plugin_manager
-  pkg_host_plugin_manager --> pkg_typert_protocol
-  pkg_host_plugin_manager --> pkg_util_values
   pkg_mcp_client --> pkg_attachment
   pkg_mcp_client --> pkg_llm
   pkg_mcp_client --> pkg_mcp_resources
@@ -1392,7 +1387,7 @@ flowchart TD
 | [`session-log-deepseek`](../packages/session/session-log-deepseek) | `session` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
 | [`session-projection`](../packages/session/session-projection) | `session` | [`session`](../packages/core/session) |
-| [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session) |
+| [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`session-snapshot`](../packages/test-support/session-snapshot) | `test-support` | [`http-proxy`](../packages/util/http-proxy), [`session`](../packages/core/session) |
 | [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol), [`util-values`](../packages/util/values) |
 | [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
@@ -1415,7 +1410,7 @@ flowchart TD
 | [`goal`](../packages/goal/goal) | `goal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) |
 | [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs) |
 | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | `fs` | [`fs`](../packages/fs/fs) |
-| [`skill-filesystem`](../packages/skill/skill-filesystem) | `skill` | [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`settings`](../packages/settings/settings), [`skill`](../packages/skill/skill) |
+| [`skill-filesystem`](../packages/skill/skill-filesystem) | `skill` | [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`skill`](../packages/skill/skill) |
 | [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`web`](../packages/web/web) |
 | [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`shell`](../packages/shell/shell) |
 | [`api-workspace-controller`](../packages/api/workspace-controller) | `api` | [`api-gateway`](../packages/api/gateway), [`client-connection`](../packages/client/connection), [`host-directory-picker`](../packages/host/directory-picker), [`session`](../packages/core/session), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) |
@@ -1423,6 +1418,7 @@ flowchart TD
 | [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
 | [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell) |
 | [`experimental-ptc-runtime-python`](../packages/experimental/ptc-runtime-python) | `experimental` | [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`timeout`](../packages/util/timeout), [`util-values`](../packages/util/values) |
+| [`host-plugin-manager`](../packages/host/plugin-manager) | `host` | [`agent`](../packages/core/agent), [`app-boot`](../packages/boot/app-boot), [`plugin-manager`](../packages/boot/plugin-manager), [`typert-protocol`](../packages/typert/protocol), [`util-values`](../packages/util/values) |
 | [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
 | [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
 | [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
@@ -1475,7 +1471,7 @@ flowchart TD
 | [`tool-jobs`](../packages/jobs/tool-jobs) | `jobs` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`tool-lsp`](../packages/lsp/tool-lsp) | `lsp` | [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
 | [`mcp-resources`](../packages/mcp/mcp-resources) | `mcp` | [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
-| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`agent`](../packages/core/agent), [`app-boot`](../packages/boot/app-boot), [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
+| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`agent`](../packages/core/agent), [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
 | [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
 | [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy) | `session` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) |
 | [`session-title-all-prompts-llm`](../packages/session/session-title-all-prompts-llm) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) |
@@ -1489,13 +1485,12 @@ flowchart TD
 | [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
 | [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | `llm` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`session`](../packages/core/session) |
 | [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-title`](../packages/session/session-title), [`tool-todo`](../packages/todo/tool-todo) |
-| [`api-settings-controller`](../packages/api/settings-controller) | `api` | [`agent-presets`](../packages/preset/agent-presets), [`credentials`](../packages/credentials/credentials), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) |
+| [`api-settings-controller`](../packages/api/settings-controller) | `api` | [`agent-presets`](../packages/preset/agent-presets), [`credentials`](../packages/credentials/credentials), [`native-command`](../packages/util/native-command), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) |
 | [`web-app`](../packages/bundle/web-app) | `bundle` | [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt) |
 | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
 | [`experimental-auto-review`](../packages/experimental/auto-review) | `experimental` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
 | [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`host-plugin-inventory`](../packages/host/plugin-inventory) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`app-boot`](../packages/boot/app-boot), [`brand`](../packages/util/brand), [`typert-protocol`](../packages/typert/protocol) |
-| [`host-plugin-manager`](../packages/host/plugin-manager) | `host` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`app-boot`](../packages/boot/app-boot), [`plugin-manager`](../packages/boot/plugin-manager), [`typert-protocol`](../packages/typert/protocol), [`util-values`](../packages/util/values) |
 | [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-resources`](../packages/mcp/mcp-resources), [`scope`](../packages/core/scope), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
 | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`command-feedback`](../packages/feedback/command-feedback), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) |
 | [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |

+ 10 - 15
docs/module-graph.zh.md

@@ -463,7 +463,6 @@ flowchart TD
   pkg_session_projection --> pkg_session
   pkg_settings --> pkg_brand
   pkg_settings --> pkg_invariants
-  pkg_settings --> pkg_scope
   pkg_settings --> pkg_session
   pkg_session_snapshot --> pkg_http_proxy
   pkg_session_snapshot --> pkg_session
@@ -553,7 +552,6 @@ flowchart TD
   pkg_fs_observation_policy --> pkg_fs
   pkg_skill_filesystem --> pkg_fs
   pkg_skill_filesystem --> pkg_home_paths
-  pkg_skill_filesystem --> pkg_settings
   pkg_skill_filesystem --> pkg_skill
   pkg_web_search_deepseek --> pkg_agent
   pkg_web_search_deepseek --> pkg_credentials
@@ -584,6 +582,11 @@ flowchart TD
   pkg_experimental_ptc_runtime_python --> pkg_ptc_runtime
   pkg_experimental_ptc_runtime_python --> pkg_timeout
   pkg_experimental_ptc_runtime_python --> pkg_util_values
+  pkg_host_plugin_manager --> pkg_agent
+  pkg_host_plugin_manager --> pkg_app_boot
+  pkg_host_plugin_manager --> pkg_plugin_manager
+  pkg_host_plugin_manager --> pkg_typert_protocol
+  pkg_host_plugin_manager --> pkg_util_values
   pkg_commands --> pkg_agent
   pkg_commands --> pkg_attachment
   pkg_commands --> pkg_brand
@@ -852,7 +855,6 @@ flowchart TD
   pkg_mcp_resources --> pkg_system_prompt
   pkg_mcp_resources --> pkg_tools
   pkg_agent_presets --> pkg_agent
-  pkg_agent_presets --> pkg_app_boot
   pkg_agent_presets --> pkg_atomic_write
   pkg_agent_presets --> pkg_home_paths
   pkg_agent_presets --> pkg_invariants
@@ -938,7 +940,6 @@ flowchart TD
   pkg_api_settings_controller --> pkg_agent_presets
   pkg_api_settings_controller --> pkg_credentials
   pkg_api_settings_controller --> pkg_native_command
-  pkg_api_settings_controller --> pkg_scope
   pkg_api_settings_controller --> pkg_session
   pkg_api_settings_controller --> pkg_settings
   pkg_api_settings_controller --> pkg_typert_protocol
@@ -965,12 +966,6 @@ flowchart TD
   pkg_host_plugin_inventory --> pkg_app_boot
   pkg_host_plugin_inventory --> pkg_brand
   pkg_host_plugin_inventory --> pkg_typert_protocol
-  pkg_host_plugin_manager --> pkg_agent
-  pkg_host_plugin_manager --> pkg_agent_presets
-  pkg_host_plugin_manager --> pkg_app_boot
-  pkg_host_plugin_manager --> pkg_plugin_manager
-  pkg_host_plugin_manager --> pkg_typert_protocol
-  pkg_host_plugin_manager --> pkg_util_values
   pkg_mcp_client --> pkg_attachment
   pkg_mcp_client --> pkg_llm
   pkg_mcp_client --> pkg_mcp_resources
@@ -1394,7 +1389,7 @@ flowchart TD
 | [`session-log-deepseek`](../packages/session/session-log-deepseek) | `session` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
 | [`session-projection`](../packages/session/session-projection) | `session` | [`session`](../packages/core/session) |
-| [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session) |
+| [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) |
 | [`session-snapshot`](../packages/test-support/session-snapshot) | `test-support` | [`http-proxy`](../packages/util/http-proxy), [`session`](../packages/core/session) |
 | [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol), [`util-values`](../packages/util/values) |
 | [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
@@ -1417,7 +1412,7 @@ flowchart TD
 | [`goal`](../packages/goal/goal) | `goal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) |
 | [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs) |
 | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | `fs` | [`fs`](../packages/fs/fs) |
-| [`skill-filesystem`](../packages/skill/skill-filesystem) | `skill` | [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`settings`](../packages/settings/settings), [`skill`](../packages/skill/skill) |
+| [`skill-filesystem`](../packages/skill/skill-filesystem) | `skill` | [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`skill`](../packages/skill/skill) |
 | [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`web`](../packages/web/web) |
 | [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`shell`](../packages/shell/shell) |
 | [`api-workspace-controller`](../packages/api/workspace-controller) | `api` | [`api-gateway`](../packages/api/gateway), [`client-connection`](../packages/client/connection), [`host-directory-picker`](../packages/host/directory-picker), [`session`](../packages/core/session), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) |
@@ -1425,6 +1420,7 @@ flowchart TD
 | [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
 | [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell) |
 | [`experimental-ptc-runtime-python`](../packages/experimental/ptc-runtime-python) | `experimental` | [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`timeout`](../packages/util/timeout), [`util-values`](../packages/util/values) |
+| [`host-plugin-manager`](../packages/host/plugin-manager) | `host` | [`agent`](../packages/core/agent), [`app-boot`](../packages/boot/app-boot), [`plugin-manager`](../packages/boot/plugin-manager), [`typert-protocol`](../packages/typert/protocol), [`util-values`](../packages/util/values) |
 | [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
 | [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
 | [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
@@ -1477,7 +1473,7 @@ flowchart TD
 | [`tool-jobs`](../packages/jobs/tool-jobs) | `jobs` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`tool-lsp`](../packages/lsp/tool-lsp) | `lsp` | [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
 | [`mcp-resources`](../packages/mcp/mcp-resources) | `mcp` | [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
-| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`agent`](../packages/core/agent), [`app-boot`](../packages/boot/app-boot), [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
+| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`agent`](../packages/core/agent), [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
 | [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools) |
 | [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy) | `session` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`tools`](../packages/core/tools) |
 | [`session-title-all-prompts-llm`](../packages/session/session-title-all-prompts-llm) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) |
@@ -1491,13 +1487,12 @@ flowchart TD
 | [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
 | [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | `llm` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`session`](../packages/core/session) |
 | [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-title`](../packages/session/session-title), [`tool-todo`](../packages/todo/tool-todo) |
-| [`api-settings-controller`](../packages/api/settings-controller) | `api` | [`agent-presets`](../packages/preset/agent-presets), [`credentials`](../packages/credentials/credentials), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) |
+| [`api-settings-controller`](../packages/api/settings-controller) | `api` | [`agent-presets`](../packages/preset/agent-presets), [`credentials`](../packages/credentials/credentials), [`native-command`](../packages/util/native-command), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) |
 | [`web-app`](../packages/bundle/web-app) | `bundle` | [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt) |
 | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
 | [`experimental-auto-review`](../packages/experimental/auto-review) | `experimental` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
 | [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`host-plugin-inventory`](../packages/host/plugin-inventory) | `host` | [`agent-presets`](../packages/preset/agent-presets), [`app-boot`](../packages/boot/app-boot), [`brand`](../packages/util/brand), [`typert-protocol`](../packages/typert/protocol) |
-| [`host-plugin-manager`](../packages/host/plugin-manager) | `host` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`app-boot`](../packages/boot/app-boot), [`plugin-manager`](../packages/boot/plugin-manager), [`typert-protocol`](../packages/typert/protocol), [`util-values`](../packages/util/values) |
 | [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-resources`](../packages/mcp/mcp-resources), [`scope`](../packages/core/scope), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
 | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`command-feedback`](../packages/feedback/command-feedback), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) |
 | [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |

+ 2 - 2
docs/subsystems/core.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/core.md
-core.md: 8058918f8cf1fd53cf424d2ddd09ea735eab63cc
-core.zh.md: e552d25f37ff9c2ba59c4f331693f5b5c88060e3
+core.md: 9ffdfdac6bc4262a541eed214386d8649ff041c1
+core.zh.md: 45298191ecaf3d03894638e3167712bf6fe39a2a

+ 5 - 30
docs/subsystems/core.md

@@ -600,28 +600,6 @@ composedPreset(agentCtx: Context): string | undefined
  */
 async read(id: string): Promise<string>
 
-/**
- * Where one preset's user patch layer is, or would be written: the layer
- * discovery attached, else the writable root's slot of the same id — beside
- * the composition for a locally authored preset, alone in the slot for a
- * shipped one. The file need not exist yet.
- * @param id - the preset id.
- * @returns the absolute path of the layer file.
- * @throws when the preset is unknown, or it has no layer and the
- * deployment configures no writable root.
- */
-async overlayPathFor(id: string): Promise<string>
-
-/**
- * Delete one preset's user patch layer, so the next generation composes the
- * preset exactly as its root supplies it. Sessions already joined keep the
- * generation they run on.
- * @param id - the preset id.
- * @returns true when a layer was removed; false when the preset had none.
- * @throws when the preset is unknown or its layer lies outside the writable root.
- */
-async removeOverlay(id: string): Promise<boolean>
-
 /**
  * One preset's composition text with the roster row it belongs to.
  * @param agentPreset - the preset id.
@@ -926,7 +904,7 @@ Source: [`packages/core/agent/src/index.ts`](../../packages/core/agent/src/index
 
 The `pluginManager` service and the `plugins` Remote.
 
-The row injects only the Loader; the profile runtime, the preset roster, and the agent registry are read off the context per call, so a composition without them (a test, a launcher other than the profile launcher) still mounts this service and every call then reports `plugins/unavailable` rather than the service failing to start.
+The row injects only the Loader; the profile runtime and the agent registry are read off the context per call, so a composition without them (a test, a launcher other than the profile launcher) still mounts this service and every call then reports `plugins/unavailable` rather than the service failing to start.
 
 ```ts cordis-catalog
 /**
@@ -971,29 +949,26 @@ The row injects only the Loader; the profile runtime, the preset roster, and the
 @Remote('retry') async retry(packageName: string): Promise<PluginEnableResult>
 
 /**
- * Add a row naming one of the package's modules to a user layer.
+ * Add a row naming one of the package's modules to the profile user layer.
  * @param packageName - the installed package.
- * @param target - which layer.
  * @param options - `module` selects a declared `dsh.plugins[]` name (default `.`),
  * `id` overrides the derived row id, `config` overrides the declared default.
  * @returns where the row landed.
  */
-@Remote('addRow') async addRow( packageName: string, target: PluginRowTarget, options?: { module?: string; id?: string; config?: JsonValue }, ): Promise<PluginRowAddition>
+@Remote('addRow') async addRow( packageName: string, options?: { module?: string; id?: string; config?: JsonValue }, ): Promise<PluginRowAddition>
 
 /**
  * Remove a row a user layer inserted.
- * @param target - which layer.
  * @param rowId - the inserted row's id.
  */
-@Remote('removeRow') async removeRow(target: PluginRowTarget, rowId: string): Promise<void>
+@Remote('removeRow') async removeRow(rowId: string): Promise<void>
 
 /**
  * Switch one row off or on in a user layer; deny-only.
- * @param target - which layer.
  * @param rowId - the row's id as the composition declares it.
  * @param disabled - whether the layer should switch the row off.
  */
-@Remote('setRowDisabled') async setRowDisabled(target: PluginRowTarget, rowId: string, disabled: boolean): Promise<void>
+@Remote('setRowDisabled') async setRowDisabled(rowId: string, disabled: boolean): Promise<void>
 
 /**
  * What disabling or removing a package would strand.

+ 5 - 30
docs/subsystems/core.zh.md

@@ -610,28 +610,6 @@ composedPreset(agentCtx: Context): string | undefined
  */
 async read(id: string): Promise<string>
 
-/**
- * Where one preset's user patch layer is, or would be written: the layer
- * discovery attached, else the writable root's slot of the same id — beside
- * the composition for a locally authored preset, alone in the slot for a
- * shipped one. The file need not exist yet.
- * @param id - the preset id.
- * @returns the absolute path of the layer file.
- * @throws when the preset is unknown, or it has no layer and the
- * deployment configures no writable root.
- */
-async overlayPathFor(id: string): Promise<string>
-
-/**
- * Delete one preset's user patch layer, so the next generation composes the
- * preset exactly as its root supplies it. Sessions already joined keep the
- * generation they run on.
- * @param id - the preset id.
- * @returns true when a layer was removed; false when the preset had none.
- * @throws when the preset is unknown or its layer lies outside the writable root.
- */
-async removeOverlay(id: string): Promise<boolean>
-
 /**
  * One preset's composition text with the roster row it belongs to.
  * @param agentPreset - the preset id.
@@ -936,7 +914,7 @@ Source: [`packages/core/agent/src/index.ts`](../../packages/core/agent/src/index
 
 The `pluginManager` service and the `plugins` Remote.
 
-The row injects only the Loader; the profile runtime, the preset roster, and the agent registry are read off the context per call, so a composition without them (a test, a launcher other than the profile launcher) still mounts this service and every call then reports `plugins/unavailable` rather than the service failing to start.
+The row injects only the Loader; the profile runtime and the agent registry are read off the context per call, so a composition without them (a test, a launcher other than the profile launcher) still mounts this service and every call then reports `plugins/unavailable` rather than the service failing to start.
 
 ```ts cordis-catalog
 /**
@@ -981,29 +959,26 @@ The row injects only the Loader; the profile runtime, the preset roster, and the
 @Remote('retry') async retry(packageName: string): Promise<PluginEnableResult>
 
 /**
- * Add a row naming one of the package's modules to a user layer.
+ * Add a row naming one of the package's modules to the profile user layer.
  * @param packageName - the installed package.
- * @param target - which layer.
  * @param options - `module` selects a declared `dsh.plugins[]` name (default `.`),
  * `id` overrides the derived row id, `config` overrides the declared default.
  * @returns where the row landed.
  */
-@Remote('addRow') async addRow( packageName: string, target: PluginRowTarget, options?: { module?: string; id?: string; config?: JsonValue }, ): Promise<PluginRowAddition>
+@Remote('addRow') async addRow( packageName: string, options?: { module?: string; id?: string; config?: JsonValue }, ): Promise<PluginRowAddition>
 
 /**
  * Remove a row a user layer inserted.
- * @param target - which layer.
  * @param rowId - the inserted row's id.
  */
-@Remote('removeRow') async removeRow(target: PluginRowTarget, rowId: string): Promise<void>
+@Remote('removeRow') async removeRow(rowId: string): Promise<void>
 
 /**
  * Switch one row off or on in a user layer; deny-only.
- * @param target - which layer.
  * @param rowId - the row's id as the composition declares it.
  * @param disabled - whether the layer should switch the row off.
  */
-@Remote('setRowDisabled') async setRowDisabled(target: PluginRowTarget, rowId: string, disabled: boolean): Promise<void>
+@Remote('setRowDisabled') async setRowDisabled(rowId: string, disabled: boolean): Promise<void>
 
 /**
  * What disabling or removing a package would strand.

+ 2 - 2
docs/subsystems/settings.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/settings.md
-settings.md: 123b099e86e64c31028d7275409ade1b2905fb94
-settings.zh.md: eee382e60787381fd26110829f483465e226899c
+settings.md: d8e3cbc46eb697315d4938b696e921a0c2e11828
+settings.zh.md: 772fd7832ff80cab9444c16c9a1dc7ae1875d51e

+ 50 - 108
docs/subsystems/settings.md

@@ -17,7 +17,7 @@ type SettingsNamespace = Branded<'SettingsNamespace'>
 
 ## Registration
 
-Registration binds a schemastery schema to a namespace on the calling plugin's fiber — disposing that fiber removes the namespace and its observers. The options carry the composition layer, the owner's effect timing, and an optional check for what the schema cannot express. A namespace is one kind of setting; each registration is an instance of it under the caller's nearest named `dsh-scope` scope (an agent preset's `preset/<id>`, or the global scope), and every registrant of a kind shares its schema. An instance resolves schema defaults, its own `base`, the document's global section, and its scope's own section, in that order.
+Registration binds a schemastery schema to a namespace on the calling plugin's fiber — disposing that fiber removes the namespace and its observers. The options carry the composition layer, the owner's effect timing, and an optional check for what the schema cannot express.
 
 ```ts type-equiv
 /** Registration options beyond the namespace schema. */
@@ -43,9 +43,6 @@ interface SettingsRegisterOptions<T> {
    * registration there is no last good value yet, so a stored section that
    * already fails rejects the registration itself — again exactly as a schema
    * failure does.
-   *
-   * The check belongs to the namespace kind: the first registrant's check
-   * judges every instance, because every instance is the same plugin.
    * @param value - the resolved section, schema-valid by construction.
    */
   validate?: (value: T) => void
@@ -68,7 +65,7 @@ The scope is the owner-facing handle. `update` merges a sparse patch over the us
 ```ts type-equiv
 /** Owner-facing handle for one registered namespace. */
 interface SettingsScope<T> {
-  /** Current resolved value: schema defaults, then `base`, then the user layers. */
+  /** Current resolved value: schema defaults, then `base`, then the user layer. */
   get(): T
   /**
    * Observe committed changes to this namespace's resolved value. Invocations
@@ -81,15 +78,14 @@ interface SettingsScope<T> {
    */
   watch(callback: (next: T, prev: T) => void | Promise<void>): () => void
   /**
-   * Merge a partial patch into this registration's user section — the scope's
-   * own section for a scoped registration — and persist it.
+   * Merge a partial patch into this namespace's user layer and persist it.
    * @param patch - plain-object patch over the user section; JSON-compatible data
    * only (non-JSON values reject with their path before anything persists).
    */
   update(patch: object): Promise<void>
   /**
-   * Replace this registration's user section wholesale; absent keys re-inherit
-   * the layers below (`replace({})` resets the section).
+   * Replace this namespace's user section wholesale; absent keys re-inherit
+   * the composition `base` and schema defaults (`replace({})` resets all).
    * @param section - the complete next user section; JSON-compatible data only,
    * as for {@link update}.
    */
@@ -104,18 +100,8 @@ interface SettingsScope<T> {
 ```ts type-equiv
 /** One registered namespace as surfaced to configuration UIs. */
 interface SettingsDescriptor {
-  // TODO(settings-namespace-vocabulary): Rename `ns` to `namespace` across the
-  // public API, provider contract, implementations, tests, and consumers.
   /** The registered namespace. */
   ns: SettingsNamespace
-  /** The named scope the descriptor resolves under; absent for the global scope. */
-  scope?: SettingsScopeId
-  /**
-   * Whether an owner registered the namespace under this scope. False for a
-   * scope no session composed yet, whose value carries no `base` of its own
-   * and resolves over the global instance's composition when one is registered.
-   */
-  registered: boolean
   /** Serialized schemastery schema (`schema.toJSON()`). */
   schema: unknown
   /** Current resolved value. */
@@ -130,15 +116,8 @@ interface SettingsDescriptor {
   /**
    * Raw user section from the stored document (detached), when one exists and
    * is well-formed; a field's presence here is what marks it user-overridden.
-   * For a scoped descriptor this is the scope's own section, not the global one.
    */
   user?: unknown
-  /**
-   * For a scoped descriptor: the value the scope resolves without its own
-   * user section — defaults, base, and the global section — so a surface can
-   * tell a field the scope overrides from one it inherits.
-   */
-  inherited?: unknown
   /** Owner's declared effect timing. */
   applies: SettingsApplies
   /** Schema-declared secret positions; present only under `redactSecrets`. */
@@ -170,13 +149,6 @@ interface SettingsDescribeOptions {
    * the verbatim default exists for same-process configuration UIs only.
    */
   redactSecrets?: boolean
-  /**
-   * Describe every namespace kind under this named scope instead of the
-   * global scope. A kind with no registration under the scope is described
-   * over the global instance's composition, else from the kind alone,
-   * `registered: false`.
-   */
-  scope?: string
 }
 ```
 
@@ -219,20 +191,13 @@ prepareDocument(): Promise<string | undefined>
 /**
  * Register a namespace schema and receive its owner scope. The registration
  * is an effect on the calling plugin's fiber: disposing that fiber removes
- * the instance and its observers. An invalid stored section fails the
+ * the namespace and its observers. An invalid stored section fails the
  * registration itself — the earliest point where the schema can judge it.
- *
- * The instance registers under the caller's nearest named scope: a plugin
- * mounted inside an agent preset resolves that preset's section over the
- * global one, and two presets mounting the same plugin hold two instances
- * of one kind. A second registrant of a namespace must carry the same
- * schema envelope; a different one is a different setting under a taken
- * name and fails loud.
- * @param ns - the namespace; a second registration under the same scope fails loud.
+ * @param ns - unique namespace; duplicate registration fails loud.
  * @param schema - schemastery schema resolving this namespace's value.
  * @param options - composition `base` layer and effect timing.
  * @returns the owner scope for reads, observation, and updates.
- * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier or is reserved.
+ * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
  */
 register<const Namespace extends string, T>( ns: Namespace & SettingsNamespaceInput<Namespace>, schema: z<T>, options?: SettingsRegisterOptions<T>, ): SettingsScope<T>
 
@@ -250,78 +215,63 @@ register<const Namespace extends string, T>( ns: Namespace & SettingsNamespaceIn
 installSection<const Namespace extends string, T>( owner: Context, ns: Namespace & SettingsNamespaceInput<Namespace>, schema: z<T>, entry: T, hooks: SettingsSectionHooks<T>, ): void
 
 /**
- * Describe every namespace kind for configuration surfaces, under the
- * global scope or one named scope: the composition `base` and raw user
- * layers so a form can mark which fields the user overrode (presence in
- * `user`) and what a reset returns to, and for a scoped read the
- * `inherited` value the scope's own section is layered over. A kind with
- * no instance under the requested scope is described from the kind alone.
- * @param options - redaction switch (wire surfaces must redact) and scope.
- * @returns one descriptor per namespace kind, in registration order.
- * @throws {TypeError} when `scope` is not a well-formed scope id.
+ * Describe every registered namespace for configuration surfaces, including
+ * the composition `base` and raw user layers so a form can mark which fields
+ * the user overrode (presence in `user`) and what a reset returns to.
+ * @param options - redaction switch; wire surfaces must redact.
+ * @returns one descriptor per registered namespace, in registration order.
  */
 describe(options?: SettingsDescribeOptions): SettingsDescriptor[]
 
-/**
- * Every named scope some namespace is registered under, in first-seen order.
- * @returns the scope ids.
- */
-scopes(): SettingsScopeId[]
-
 /**
  * Read one registered namespace's resolved value.
  * @param ns - the namespace to read.
- * @param scope - the named scope of the instance; the global instance when omitted.
- * @returns the resolved value, or `undefined` while unregistered under that scope.
+ * @returns the resolved value, or `undefined` while unregistered.
  * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
  */
-get<const Namespace extends string>(ns: Namespace & SettingsNamespaceInput<Namespace>, scope?: string): unknown
+get<const Namespace extends string>(ns: Namespace & SettingsNamespaceInput<Namespace>): unknown
 
 /**
- * Merge a patch into one namespace's user section, validate the resolved
- * candidates, persist through the provider, then commit and emit. A
- * validation failure rejects before anything is persisted. Writes to one
- * section are serialized: concurrent updates apply in call order, each
- * merging over the previous write's committed section. A global write
- * re-resolves every instance of the kind; a scoped write only that scope's.
+ * Merge a patch into one registered namespace's user layer, validate the
+ * resolved candidate, persist through the provider, then commit and emit.
+ * A validation failure rejects before anything is persisted. Writes to one
+ * namespace are serialized: concurrent updates apply in call order, each
+ * merging over the previous write's committed section.
  * @param ns - the registered namespace to update.
  * @param patch - plain-object patch over the user section.
  * @param expectedRevision - the descriptor `revision` the caller read; a
- *   section that moved past it rejects with {@link SettingsConflictError}.
- * @param scope - the named scope whose section to write; the global section when omitted.
+ *   namespace that moved past it rejects with {@link SettingsConflictError}.
  * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
  */
-async update<const Namespace extends string>( ns: Namespace & SettingsNamespaceInput<Namespace>, patch: object, expectedRevision?: number, scope?: string, ): Promise<void>
+async update<const Namespace extends string>( ns: Namespace & SettingsNamespaceInput<Namespace>, patch: object, expectedRevision?: number, ): Promise<void>
 
 /**
- * Replace one namespace's user section wholesale, validate, persist, then
- * commit and emit. Keys absent from `section` fall back to the layers
- * below — this is the removal/reset path a merge-only patch cannot express
- * (`replace({})` re-inherits everything).
+ * Replace one registered namespace's user section wholesale, validate,
+ * persist, then commit and emit. Keys absent from `section` fall back to the
+ * composition `base` and schema defaults — this is the removal/reset path a
+ * merge-only patch cannot express (`replace({})` re-inherits everything).
  * @param ns - the registered namespace to replace.
  * @param section - the complete next user section.
  * @param expectedRevision - the descriptor `revision` the caller read; a
- *   section that moved past it rejects with {@link SettingsConflictError}.
- * @param scope - the named scope whose section to write; the global section when omitted.
+ *   namespace that moved past it rejects with {@link SettingsConflictError}.
  * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
  */
-async replace<const Namespace extends string>( ns: Namespace & SettingsNamespaceInput<Namespace>, section: object, expectedRevision?: number, scope?: string, ): Promise<void>
+async replace<const Namespace extends string>( ns: Namespace & SettingsNamespaceInput<Namespace>, section: object, expectedRevision?: number, ): Promise<void>
 
 /**
- * Apply path-addressed edits to one namespace's user section, validate,
- * persist, then commit and emit. The ops are applied to the section as it
- * stands when the write reaches the front of the queue, so a caller never
- * has to restate fields it did not touch — and, crucially, cannot delete
- * fields it never saw. This is the write path for any caller holding a
- * redacted view; `replace` remains the wholesale reset.
+ * Apply path-addressed edits to one registered namespace's user section,
+ * validate, persist, then commit and emit. The ops are applied to the
+ * section as it stands when the write reaches the front of the queue, so a
+ * caller never has to restate fields it did not touch — and, crucially,
+ * cannot delete fields it never saw. This is the write path for any caller
+ * holding a redacted view; `replace` remains the wholesale reset.
  * @param ns - the registered namespace to edit.
  * @param ops - ordered path edits; later ops observe earlier ones.
  * @param expectedRevision - the descriptor `revision` the caller read; a
- *   section that moved past it rejects with {@link SettingsConflictError}.
- * @param scope - the named scope whose section to write; the global section when omitted.
+ *   namespace that moved past it rejects with {@link SettingsConflictError}.
  * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
  */
-async mutate<const Namespace extends string>( ns: Namespace & SettingsNamespaceInput<Namespace>, ops: readonly SettingsPathOp[], expectedRevision?: number, scope?: string, ): Promise<void>
+async mutate<const Namespace extends string>( ns: Namespace & SettingsNamespaceInput<Namespace>, ops: readonly SettingsPathOp[], expectedRevision?: number, ): Promise<void>
 ```
 
 Source: [`packages/settings/settings/src/index.ts`](../../packages/settings/settings/src/index.ts)
@@ -334,15 +284,12 @@ Host service backing the generated `ctx.remote.settings` namespace. Every remote
 
 ```ts cordis-catalog
 /**
- * Describe every namespace kind for a configuration page: redacted layered
- * values plus the serialized schema the page renders its form from, under
- * the global scope or one named scope (an agent preset's `preset/<id>`).
- * @param scope - the named scope to describe; the global scope when omitted.
- * @returns provider writability, local-document presence, one view per
- * namespace kind, and every scope some namespace is registered under.
- * @throws RemoteError when no settings provider is mounted or the scope id is malformed.
+ * Describe every registered namespace for a configuration page: redacted
+ * layered values plus the serialized schema the page renders its form from.
+ * @returns provider writability, local-document presence, and one view per namespace.
+ * @throws RemoteError when no settings provider is mounted.
  */
-@Remote describe(scope?: string): SettingsDescribeValue
+@Remote describe(): SettingsDescribeValue
 
 /**
  * Report whether this deployment can open an authored Agent preset directory natively.
@@ -355,22 +302,20 @@ Host service backing the generated `ctx.remote.settings` namespace. Every remote
  * @param ns - namespace key to write.
  * @param patch - fields to merge into the user section.
  * @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
- * @param scope - the named scope whose section to write; the global section when omitted.
- * @returns the namespace's redacted view under that scope after the write.
+ * @returns the namespace's redacted view after the write.
  * @throws RemoteError when the request is invalid, no provider is mounted, or the provider refuses the write.
  */
-@Remote update( ns: string, patch: Record<string, JsonValue>, expectedRevision: number | undefined, scope?: string, ): Promise<SettingsNamespaceView>
+@Remote update( ns: string, patch: Record<string, JsonValue>, expectedRevision: number | undefined, ): Promise<SettingsNamespaceView>
 
 /**
  * Replace one namespace's stored user section wholesale.
  * @param ns - namespace key to write.
  * @param section - complete replacement user section.
  * @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
- * @param scope - the named scope whose section to write; the global section when omitted.
- * @returns the namespace's redacted view under that scope after the write.
+ * @returns the namespace's redacted view after the write.
  * @throws RemoteError when the request is invalid, no provider is mounted, or the provider refuses the write.
  */
-@Remote replace( ns: string, section: Record<string, JsonValue>, expectedRevision: number | undefined, scope?: string, ): Promise<SettingsNamespaceView>
+@Remote replace( ns: string, section: Record<string, JsonValue>, expectedRevision: number | undefined, ): Promise<SettingsNamespaceView>
 
 /**
  * Apply path-addressed edits to one namespace's user section, resolved against
@@ -379,11 +324,10 @@ Host service backing the generated `ctx.remote.settings` namespace. Every remote
  * @param ns - namespace key to write.
  * @param ops - the edits to apply, in order.
  * @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
- * @param scope - the named scope whose section to write; the global section when omitted.
- * @returns the namespace's redacted view under that scope after the write.
+ * @returns the namespace's redacted view after the write.
  * @throws RemoteError when the request is invalid, no provider is mounted, or the provider refuses the write.
  */
-@Remote async mutate( ns: string, ops: SettingsPathOpView[], expectedRevision: number | undefined, scope?: string, ): Promise<SettingsNamespaceView>
+@Remote async mutate( ns: string, ops: SettingsPathOpView[], expectedRevision: number | undefined, ): Promise<SettingsNamespaceView>
 
 /**
  * Materialize the provider-owned settings document and open it in a native text editor.
@@ -424,11 +368,10 @@ One registered namespace's RAW user section changed, whether or not the resolved
  * resolved value, different meaning) and that their held revision is
  * stale. Listener containment matches `settings/updated`.
  * @param ns - the namespace whose stored section changed.
- * @param revision - the section's new revision.
- * @param scope - the named scope whose section changed; absent for the global section.
+ * @param revision - the namespace's new revision.
  * @mode emit
  */
-'settings/document-updated'(ns: SettingsNamespace, revision: number, scope?: SettingsScopeId): void
+'settings/document-updated'(ns: SettingsNamespace, revision: number): void
 ```
 
 Source: [`packages/settings/settings/src/types.ts`](../../packages/settings/settings/src/types.ts)
@@ -453,10 +396,9 @@ Committed change to one registered namespace's resolved value. Emitted after the
  * @param next - the new resolved value.
  * @param prev - the previous resolved value.
  * @param source - whether the change entered through `update()` or the provider.
- * @param scope - the named scope whose registration changed; absent for the global scope.
  * @mode emit
  */
-'settings/updated'(ns: SettingsNamespace, next: unknown, prev: unknown, source: SettingsUpdateSource, scope?: SettingsScopeId): void
+'settings/updated'(ns: SettingsNamespace, next: unknown, prev: unknown, source: SettingsUpdateSource): void
 ```
 
 Source: [`packages/settings/settings/src/types.ts`](../../packages/settings/settings/src/types.ts)

+ 50 - 108
docs/subsystems/settings.zh.md

@@ -17,7 +17,7 @@ type SettingsNamespace = Branded<'SettingsNamespace'>
 
 ## 注册
 
-注册把 schemastery schema 绑定到调用方插件 fiber 上的 namespace——dispose(资源释放)该 fiber 即移除 namespace 及其观察者。options 携带组合层、owner 的生效时机,以及一个可选的、用于校验 schema 表达不了的约束的钩子。namespace 是一种设置的 kind;每次注册是它在调用方最近的具名 `dsh-scope` 作用域(agent preset 的 `preset/<id>`,或全局作用域)下的一个 instance,同一 kind 的每个注册者共享其 schema。instance 按顺序解析 schema 默认值、自己的 `base`、文档的全局分节与其作用域自己的分节。
+注册把 schemastery schema 绑定到调用方插件 fiber 上的 namespace——dispose(资源释放)该 fiber 即移除 namespace 及其观察者。options 携带组合层、owner 的生效时机,以及一个可选的、用于校验 schema 表达不了的约束的钩子。
 
 ```ts type-equiv
 /** Registration options beyond the namespace schema. */
@@ -43,9 +43,6 @@ interface SettingsRegisterOptions<T> {
    * registration there is no last good value yet, so a stored section that
    * already fails rejects the registration itself — again exactly as a schema
    * failure does.
-   *
-   * The check belongs to the namespace kind: the first registrant's check
-   * judges every instance, because every instance is the same plugin.
    * @param value - the resolved section, schema-valid by construction.
    */
   validate?: (value: T) => void
@@ -68,7 +65,7 @@ scope 是面向 owner 的句柄。`update` 把稀疏 patch 只合并进用户分
 ```ts type-equiv
 /** Owner-facing handle for one registered namespace. */
 interface SettingsScope<T> {
-  /** Current resolved value: schema defaults, then `base`, then the user layers. */
+  /** Current resolved value: schema defaults, then `base`, then the user layer. */
   get(): T
   /**
    * Observe committed changes to this namespace's resolved value. Invocations
@@ -81,15 +78,14 @@ interface SettingsScope<T> {
    */
   watch(callback: (next: T, prev: T) => void | Promise<void>): () => void
   /**
-   * Merge a partial patch into this registration's user section — the scope's
-   * own section for a scoped registration — and persist it.
+   * Merge a partial patch into this namespace's user layer and persist it.
    * @param patch - plain-object patch over the user section; JSON-compatible data
    * only (non-JSON values reject with their path before anything persists).
    */
   update(patch: object): Promise<void>
   /**
-   * Replace this registration's user section wholesale; absent keys re-inherit
-   * the layers below (`replace({})` resets the section).
+   * Replace this namespace's user section wholesale; absent keys re-inherit
+   * the composition `base` and schema defaults (`replace({})` resets all).
    * @param section - the complete next user section; JSON-compatible data only,
    * as for {@link update}.
    */
@@ -104,18 +100,8 @@ interface SettingsScope<T> {
 ```ts type-equiv
 /** One registered namespace as surfaced to configuration UIs. */
 interface SettingsDescriptor {
-  // TODO(settings-namespace-vocabulary): Rename `ns` to `namespace` across the
-  // public API, provider contract, implementations, tests, and consumers.
   /** The registered namespace. */
   ns: SettingsNamespace
-  /** The named scope the descriptor resolves under; absent for the global scope. */
-  scope?: SettingsScopeId
-  /**
-   * Whether an owner registered the namespace under this scope. False for a
-   * scope no session composed yet, whose value carries no `base` of its own
-   * and resolves over the global instance's composition when one is registered.
-   */
-  registered: boolean
   /** Serialized schemastery schema (`schema.toJSON()`). */
   schema: unknown
   /** Current resolved value. */
@@ -130,15 +116,8 @@ interface SettingsDescriptor {
   /**
    * Raw user section from the stored document (detached), when one exists and
    * is well-formed; a field's presence here is what marks it user-overridden.
-   * For a scoped descriptor this is the scope's own section, not the global one.
    */
   user?: unknown
-  /**
-   * For a scoped descriptor: the value the scope resolves without its own
-   * user section — defaults, base, and the global section — so a surface can
-   * tell a field the scope overrides from one it inherits.
-   */
-  inherited?: unknown
   /** Owner's declared effect timing. */
   applies: SettingsApplies
   /** Schema-declared secret positions; present only under `redactSecrets`. */
@@ -170,13 +149,6 @@ interface SettingsDescribeOptions {
    * the verbatim default exists for same-process configuration UIs only.
    */
   redactSecrets?: boolean
-  /**
-   * Describe every namespace kind under this named scope instead of the
-   * global scope. A kind with no registration under the scope is described
-   * over the global instance's composition, else from the kind alone,
-   * `registered: false`.
-   */
-  scope?: string
 }
 ```
 
@@ -219,20 +191,13 @@ prepareDocument(): Promise<string | undefined>
 /**
  * Register a namespace schema and receive its owner scope. The registration
  * is an effect on the calling plugin's fiber: disposing that fiber removes
- * the instance and its observers. An invalid stored section fails the
+ * the namespace and its observers. An invalid stored section fails the
  * registration itself — the earliest point where the schema can judge it.
- *
- * The instance registers under the caller's nearest named scope: a plugin
- * mounted inside an agent preset resolves that preset's section over the
- * global one, and two presets mounting the same plugin hold two instances
- * of one kind. A second registrant of a namespace must carry the same
- * schema envelope; a different one is a different setting under a taken
- * name and fails loud.
- * @param ns - the namespace; a second registration under the same scope fails loud.
+ * @param ns - unique namespace; duplicate registration fails loud.
  * @param schema - schemastery schema resolving this namespace's value.
  * @param options - composition `base` layer and effect timing.
  * @returns the owner scope for reads, observation, and updates.
- * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier or is reserved.
+ * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
  */
 register<const Namespace extends string, T>( ns: Namespace & SettingsNamespaceInput<Namespace>, schema: z<T>, options?: SettingsRegisterOptions<T>, ): SettingsScope<T>
 
@@ -250,78 +215,63 @@ register<const Namespace extends string, T>( ns: Namespace & SettingsNamespaceIn
 installSection<const Namespace extends string, T>( owner: Context, ns: Namespace & SettingsNamespaceInput<Namespace>, schema: z<T>, entry: T, hooks: SettingsSectionHooks<T>, ): void
 
 /**
- * Describe every namespace kind for configuration surfaces, under the
- * global scope or one named scope: the composition `base` and raw user
- * layers so a form can mark which fields the user overrode (presence in
- * `user`) and what a reset returns to, and for a scoped read the
- * `inherited` value the scope's own section is layered over. A kind with
- * no instance under the requested scope is described from the kind alone.
- * @param options - redaction switch (wire surfaces must redact) and scope.
- * @returns one descriptor per namespace kind, in registration order.
- * @throws {TypeError} when `scope` is not a well-formed scope id.
+ * Describe every registered namespace for configuration surfaces, including
+ * the composition `base` and raw user layers so a form can mark which fields
+ * the user overrode (presence in `user`) and what a reset returns to.
+ * @param options - redaction switch; wire surfaces must redact.
+ * @returns one descriptor per registered namespace, in registration order.
  */
 describe(options?: SettingsDescribeOptions): SettingsDescriptor[]
 
-/**
- * Every named scope some namespace is registered under, in first-seen order.
- * @returns the scope ids.
- */
-scopes(): SettingsScopeId[]
-
 /**
  * Read one registered namespace's resolved value.
  * @param ns - the namespace to read.
- * @param scope - the named scope of the instance; the global instance when omitted.
- * @returns the resolved value, or `undefined` while unregistered under that scope.
+ * @returns the resolved value, or `undefined` while unregistered.
  * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
  */
-get<const Namespace extends string>(ns: Namespace & SettingsNamespaceInput<Namespace>, scope?: string): unknown
+get<const Namespace extends string>(ns: Namespace & SettingsNamespaceInput<Namespace>): unknown
 
 /**
- * Merge a patch into one namespace's user section, validate the resolved
- * candidates, persist through the provider, then commit and emit. A
- * validation failure rejects before anything is persisted. Writes to one
- * section are serialized: concurrent updates apply in call order, each
- * merging over the previous write's committed section. A global write
- * re-resolves every instance of the kind; a scoped write only that scope's.
+ * Merge a patch into one registered namespace's user layer, validate the
+ * resolved candidate, persist through the provider, then commit and emit.
+ * A validation failure rejects before anything is persisted. Writes to one
+ * namespace are serialized: concurrent updates apply in call order, each
+ * merging over the previous write's committed section.
  * @param ns - the registered namespace to update.
  * @param patch - plain-object patch over the user section.
  * @param expectedRevision - the descriptor `revision` the caller read; a
- *   section that moved past it rejects with {@link SettingsConflictError}.
- * @param scope - the named scope whose section to write; the global section when omitted.
+ *   namespace that moved past it rejects with {@link SettingsConflictError}.
  * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
  */
-async update<const Namespace extends string>( ns: Namespace & SettingsNamespaceInput<Namespace>, patch: object, expectedRevision?: number, scope?: string, ): Promise<void>
+async update<const Namespace extends string>( ns: Namespace & SettingsNamespaceInput<Namespace>, patch: object, expectedRevision?: number, ): Promise<void>
 
 /**
- * Replace one namespace's user section wholesale, validate, persist, then
- * commit and emit. Keys absent from `section` fall back to the layers
- * below — this is the removal/reset path a merge-only patch cannot express
- * (`replace({})` re-inherits everything).
+ * Replace one registered namespace's user section wholesale, validate,
+ * persist, then commit and emit. Keys absent from `section` fall back to the
+ * composition `base` and schema defaults — this is the removal/reset path a
+ * merge-only patch cannot express (`replace({})` re-inherits everything).
  * @param ns - the registered namespace to replace.
  * @param section - the complete next user section.
  * @param expectedRevision - the descriptor `revision` the caller read; a
- *   section that moved past it rejects with {@link SettingsConflictError}.
- * @param scope - the named scope whose section to write; the global section when omitted.
+ *   namespace that moved past it rejects with {@link SettingsConflictError}.
  * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
  */
-async replace<const Namespace extends string>( ns: Namespace & SettingsNamespaceInput<Namespace>, section: object, expectedRevision?: number, scope?: string, ): Promise<void>
+async replace<const Namespace extends string>( ns: Namespace & SettingsNamespaceInput<Namespace>, section: object, expectedRevision?: number, ): Promise<void>
 
 /**
- * Apply path-addressed edits to one namespace's user section, validate,
- * persist, then commit and emit. The ops are applied to the section as it
- * stands when the write reaches the front of the queue, so a caller never
- * has to restate fields it did not touch — and, crucially, cannot delete
- * fields it never saw. This is the write path for any caller holding a
- * redacted view; `replace` remains the wholesale reset.
+ * Apply path-addressed edits to one registered namespace's user section,
+ * validate, persist, then commit and emit. The ops are applied to the
+ * section as it stands when the write reaches the front of the queue, so a
+ * caller never has to restate fields it did not touch — and, crucially,
+ * cannot delete fields it never saw. This is the write path for any caller
+ * holding a redacted view; `replace` remains the wholesale reset.
  * @param ns - the registered namespace to edit.
  * @param ops - ordered path edits; later ops observe earlier ones.
  * @param expectedRevision - the descriptor `revision` the caller read; a
- *   section that moved past it rejects with {@link SettingsConflictError}.
- * @param scope - the named scope whose section to write; the global section when omitted.
+ *   namespace that moved past it rejects with {@link SettingsConflictError}.
  * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
  */
-async mutate<const Namespace extends string>( ns: Namespace & SettingsNamespaceInput<Namespace>, ops: readonly SettingsPathOp[], expectedRevision?: number, scope?: string, ): Promise<void>
+async mutate<const Namespace extends string>( ns: Namespace & SettingsNamespaceInput<Namespace>, ops: readonly SettingsPathOp[], expectedRevision?: number, ): Promise<void>
 ```
 
 Source: [`packages/settings/settings/src/index.ts`](../../packages/settings/settings/src/index.ts)
@@ -334,15 +284,12 @@ Host service backing the generated `ctx.remote.settings` namespace. Every remote
 
 ```ts cordis-catalog
 /**
- * Describe every namespace kind for a configuration page: redacted layered
- * values plus the serialized schema the page renders its form from, under
- * the global scope or one named scope (an agent preset's `preset/<id>`).
- * @param scope - the named scope to describe; the global scope when omitted.
- * @returns provider writability, local-document presence, one view per
- * namespace kind, and every scope some namespace is registered under.
- * @throws RemoteError when no settings provider is mounted or the scope id is malformed.
+ * Describe every registered namespace for a configuration page: redacted
+ * layered values plus the serialized schema the page renders its form from.
+ * @returns provider writability, local-document presence, and one view per namespace.
+ * @throws RemoteError when no settings provider is mounted.
  */
-@Remote describe(scope?: string): SettingsDescribeValue
+@Remote describe(): SettingsDescribeValue
 
 /**
  * Report whether this deployment can open an authored Agent preset directory natively.
@@ -355,22 +302,20 @@ Host service backing the generated `ctx.remote.settings` namespace. Every remote
  * @param ns - namespace key to write.
  * @param patch - fields to merge into the user section.
  * @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
- * @param scope - the named scope whose section to write; the global section when omitted.
- * @returns the namespace's redacted view under that scope after the write.
+ * @returns the namespace's redacted view after the write.
  * @throws RemoteError when the request is invalid, no provider is mounted, or the provider refuses the write.
  */
-@Remote update( ns: string, patch: Record<string, JsonValue>, expectedRevision: number | undefined, scope?: string, ): Promise<SettingsNamespaceView>
+@Remote update( ns: string, patch: Record<string, JsonValue>, expectedRevision: number | undefined, ): Promise<SettingsNamespaceView>
 
 /**
  * Replace one namespace's stored user section wholesale.
  * @param ns - namespace key to write.
  * @param section - complete replacement user section.
  * @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
- * @param scope - the named scope whose section to write; the global section when omitted.
- * @returns the namespace's redacted view under that scope after the write.
+ * @returns the namespace's redacted view after the write.
  * @throws RemoteError when the request is invalid, no provider is mounted, or the provider refuses the write.
  */
-@Remote replace( ns: string, section: Record<string, JsonValue>, expectedRevision: number | undefined, scope?: string, ): Promise<SettingsNamespaceView>
+@Remote replace( ns: string, section: Record<string, JsonValue>, expectedRevision: number | undefined, ): Promise<SettingsNamespaceView>
 
 /**
  * Apply path-addressed edits to one namespace's user section, resolved against
@@ -379,11 +324,10 @@ Host service backing the generated `ctx.remote.settings` namespace. Every remote
  * @param ns - namespace key to write.
  * @param ops - the edits to apply, in order.
  * @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
- * @param scope - the named scope whose section to write; the global section when omitted.
- * @returns the namespace's redacted view under that scope after the write.
+ * @returns the namespace's redacted view after the write.
  * @throws RemoteError when the request is invalid, no provider is mounted, or the provider refuses the write.
  */
-@Remote async mutate( ns: string, ops: SettingsPathOpView[], expectedRevision: number | undefined, scope?: string, ): Promise<SettingsNamespaceView>
+@Remote async mutate( ns: string, ops: SettingsPathOpView[], expectedRevision: number | undefined, ): Promise<SettingsNamespaceView>
 
 /**
  * Materialize the provider-owned settings document and open it in a native text editor.
@@ -424,11 +368,10 @@ One registered namespace's RAW user section changed, whether or not the resolved
  * resolved value, different meaning) and that their held revision is
  * stale. Listener containment matches `settings/updated`.
  * @param ns - the namespace whose stored section changed.
- * @param revision - the section's new revision.
- * @param scope - the named scope whose section changed; absent for the global section.
+ * @param revision - the namespace's new revision.
  * @mode emit
  */
-'settings/document-updated'(ns: SettingsNamespace, revision: number, scope?: SettingsScopeId): void
+'settings/document-updated'(ns: SettingsNamespace, revision: number): void
 ```
 
 Source: [`packages/settings/settings/src/types.ts`](../../packages/settings/settings/src/types.ts)
@@ -453,10 +396,9 @@ Committed change to one registered namespace's resolved value. Emitted after the
  * @param next - the new resolved value.
  * @param prev - the previous resolved value.
  * @param source - whether the change entered through `update()` or the provider.
- * @param scope - the named scope whose registration changed; absent for the global scope.
  * @mode emit
  */
-'settings/updated'(ns: SettingsNamespace, next: unknown, prev: unknown, source: SettingsUpdateSource, scope?: SettingsScopeId): void
+'settings/updated'(ns: SettingsNamespace, next: unknown, prev: unknown, source: SettingsUpdateSource): void
 ```
 
 Source: [`packages/settings/settings/src/types.ts`](../../packages/settings/settings/src/types.ts)

+ 2 - 2
packages/api/settings-controller/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/api/settings-controller/README.md
-README.md: 3a458bbb0cd20730cb530195883d13afc601dbe7
-README.zh.md: 51965c9c6f8c79d55200bb2b5fd494e5ea92ff68
+README.md: 761e4c43751675eec141a2efcf85caf52cc91590
+README.zh.md: c5a59f3eebfd1e6702c2abcd2407bc2ef4e725bd

+ 1 - 1
packages/api/settings-controller/README.md

@@ -27,7 +27,7 @@ Mount this package as a Loader entry in a profile that serves browser configurat
 
 `describe(refs)` answers one map keyed by the requested names, so a settings page describing every reference its rows carry settles those rows together. It accepts at most 64 names per call, reports an invalid name or empty write value as `bad-request`, and copies each answer field by field — a provider returning more than `CredentialInfo` declares cannot widen what crosses. Valid `set(ref, value)` and `unset(ref)` calls report a provider refusal as `credential-rejected`, carrying the provider's message with only the reference in its details. Secret values cross in this direction only: no method here returns one.
 
-`settings.describe(scope?)` returns deployment facts and every namespace kind under `redactSecrets: true` — under the global scope, or under one named scope such as an agent preset's `preset/<id>`, where each view says whether an owner registered the namespace there and carries the `inherited` value the scope's own section is layered over — together with every scope some namespace is registered under. `settings.update`, `settings.replace`, and `settings.mutate` expose the settings service's three write operations, take the same optional trailing `scope`, and return the namespace's new redacted view under that scope; stale writes use `settings-conflict` and other provider refusals use `settings-rejected`.
+`settings.describe()` returns deployment facts and every namespace under `redactSecrets: true`. `settings.update`, `settings.replace`, and `settings.mutate` expose the settings service's three write operations and return the namespace's new redacted view; stale writes use `settings-conflict` and other provider refusals use `settings-rejected`.
 
 `settings.openSettingsDocument()` prepares the provider-owned document and opens it with the native text-editor intent. `settings.canOpenAgentPresetDirectory()` reports native-opening availability when the preset page becomes visible. `settings.openAgentPresetDirectory(id)` resolves only a user-authored preset and either opens its directory or returns the path when native opening is unavailable; neither open method accepts a browser-supplied filesystem target.
 

+ 1 - 1
packages/api/settings-controller/README.zh.md

@@ -27,7 +27,7 @@ kind: "package-reference"
 
 `describe(refs)` 以请求的名字为键返回一份 map,因此设置页描述其各行携带的全部引用时,这些行会一起落定。单次调用最多接受 64 个名字,无效名字或空写入值报告为 `bad-request`,并逐字段复制每个答案——提供方返回超出 `CredentialInfo` 声明的内容也无法扩大跨越 wire 的字段。有效的 `set(ref, value)` 与 `unset(ref)` 调用把提供方拒绝报告为 `credential-rejected`,携带提供方的消息,details 中只有该引用。机密值只在这个方向跨越 wire:这里没有任何方法会返回它。
 
-`settings.describe(scope?)` 返回部署信息,以及在 `redactSecrets: true` 下读取的所有 namespace kind——在全局 scope 下,或在某个具名 scope(如 agent preset 的 `preset/<id>`)下,此时每条视图都说明该 scope 下是否有 owner 注册了此 namespace,并携带该 scope 自己分节所叠加的 `inherited` 值——连同所有有 namespace 注册于其下的 scope。`settings.update`、`settings.replace` 与 `settings.mutate` 暴露 settings service 的三种写入操作,接受同样的可选尾随 `scope`,并返回该 namespace 在该 scope 下的新脱敏视图;过期写入使用 `settings-conflict`,其他 provider 拒绝使用 `settings-rejected`。
+`settings.describe()` 返回部署信息,以及在 `redactSecrets: true` 下读取的所有 namespace。`settings.update`、`settings.replace` 与 `settings.mutate` 暴露 settings 服务的三种写入操作,并返回该 namespace 的新脱敏视图;陈旧写入使用 `settings-conflict`,其他提供方拒绝使用 `settings-rejected`。
 
 `settings.openSettingsDocument()` 准备提供方持有的文档,并用原生文本编辑器意图将其打开。`settings.canOpenAgentPresetDirectory()` 在 preset 页面显示时报告原生打开能力。`settings.openAgentPresetDirectory(id)` 只解析用户创作的 preset,并打开其目录,或在原生打开不可用时返回目录路径;两个打开方法都不接受浏览器提供的文件系统目标。
 

+ 0 - 2
packages/api/settings-controller/package.json

@@ -52,7 +52,6 @@
     "@deepseek-ai/dsh-agent-presets": "workspace:^",
     "@deepseek-ai/dsh-credentials": "workspace:^",
     "@deepseek-ai/dsh-native-command": "workspace:^",
-    "@deepseek-ai/dsh-scope": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
     "@deepseek-ai/dsh-settings": "workspace:^",
     "@deepseek-ai/dsh-typert-protocol": "workspace:^"
@@ -62,7 +61,6 @@
     "@deepseek-ai/dsh-agent-presets": "workspace:^",
     "@deepseek-ai/dsh-credentials": "workspace:^",
     "@deepseek-ai/dsh-native-command": "workspace:^",
-    "@deepseek-ai/dsh-scope": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
     "@deepseek-ai/dsh-settings": "workspace:^",
     "@deepseek-ai/dsh-typert-protocol": "workspace:^"

+ 18 - 41
packages/api/settings-controller/src/index.ts

@@ -30,7 +30,7 @@ import type { AgentPresetDirectoryOpenValue, SettingsDocumentOpenValue } from '.
 export { CredentialsController } from './credentials.ts'
 export type * from './types.ts'
 
-const settingsNamespaceRequestSchema = z.object({ ns: z.string().min(1), scope: z.string().min(1).optional() })
+const settingsNamespaceRequestSchema = z.object({ ns: z.string().min(1) })
 
 /** Native document-opening policy. */
 export interface Config {
@@ -61,13 +61,10 @@ export interface SettingsControllerInternals {
 function namespaceView(descriptor: SettingsDescriptor): SettingsNamespaceView {
   return {
     ns: String(descriptor.ns),
-    ...descriptor.scope === undefined ? {} : { scope: String(descriptor.scope) },
-    registered: descriptor.registered,
     schema: descriptor.schema as JsonValue,
     value: descriptor.value as JsonValue,
     ...descriptor.base === undefined ? {} : { base: descriptor.base as JsonValue },
     ...descriptor.user === undefined ? {} : { user: descriptor.user as JsonValue },
-    ...descriptor.inherited === undefined ? {} : { inherited: descriptor.inherited as JsonValue },
     applies: descriptor.applies,
     secrets: (descriptor.secrets ?? []).map(secret => ({ path: [...secret.path], set: secret.set })),
     revision: descriptor.revision,
@@ -111,29 +108,18 @@ export class SettingsController extends TypertRemoteService {
   }
 
   /**
-   * Describe every namespace kind for a configuration page: redacted layered
-   * values plus the serialized schema the page renders its form from, under
-   * the global scope or one named scope (an agent preset's `preset/<id>`).
-   * @param scope - the named scope to describe; the global scope when omitted.
-   * @returns provider writability, local-document presence, one view per
-   * namespace kind, and every scope some namespace is registered under.
-   * @throws RemoteError when no settings provider is mounted or the scope id is malformed.
+   * Describe every registered namespace for a configuration page: redacted
+   * layered values plus the serialized schema the page renders its form from.
+   * @returns provider writability, local-document presence, and one view per namespace.
+   * @throws RemoteError when no settings provider is mounted.
    */
   @Remote
-  describe(scope?: string): SettingsDescribeValue {
+  describe(): SettingsDescribeValue {
     const settings = this.provider()
-    let views: SettingsNamespaceView[]
-    try {
-      views = settings.describe({ redactSecrets: true, ...scope === undefined ? {} : { scope } }).map(namespaceView)
-    } catch (error: unknown) {
-      throw new RemoteError('gateway/bad-request', `invalid scope for settings.describe: ${messageOf(error)}`, {}, { cause: error })
-    }
     return {
       writable: settings.writable,
       hasDocument: settings.documentPath !== undefined,
-      namespaces: views,
-      ...scope === undefined ? {} : { scope },
-      scopes: settings.scopes().map(String),
+      namespaces: settings.describe({ redactSecrets: true }).map(namespaceView),
     }
   }
 
@@ -151,8 +137,7 @@ export class SettingsController extends TypertRemoteService {
    * @param ns - namespace key to write.
    * @param patch - fields to merge into the user section.
    * @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
-   * @param scope - the named scope whose section to write; the global section when omitted.
-   * @returns the namespace's redacted view under that scope after the write.
+   * @returns the namespace's redacted view after the write.
    * @throws RemoteError when the request is invalid, no provider is mounted, or the provider refuses the write.
    */
   @Remote
@@ -160,9 +145,8 @@ export class SettingsController extends TypertRemoteService {
     ns: string,
     patch: Record<string, JsonValue>,
     expectedRevision: number | undefined,
-    scope?: string,
   ): Promise<SettingsNamespaceView> {
-    return this.write(ns, 'update', patch, expectedRevision, scope)
+    return this.write(ns, 'update', patch, expectedRevision)
   }
 
   /**
@@ -170,8 +154,7 @@ export class SettingsController extends TypertRemoteService {
    * @param ns - namespace key to write.
    * @param section - complete replacement user section.
    * @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
-   * @param scope - the named scope whose section to write; the global section when omitted.
-   * @returns the namespace's redacted view under that scope after the write.
+   * @returns the namespace's redacted view after the write.
    * @throws RemoteError when the request is invalid, no provider is mounted, or the provider refuses the write.
    */
   @Remote
@@ -179,9 +162,8 @@ export class SettingsController extends TypertRemoteService {
     ns: string,
     section: Record<string, JsonValue>,
     expectedRevision: number | undefined,
-    scope?: string,
   ): Promise<SettingsNamespaceView> {
-    return this.write(ns, 'replace', section, expectedRevision, scope)
+    return this.write(ns, 'replace', section, expectedRevision)
   }
 
   /**
@@ -191,8 +173,7 @@ export class SettingsController extends TypertRemoteService {
    * @param ns - namespace key to write.
    * @param ops - the edits to apply, in order.
    * @param expectedRevision - revision the caller read; `undefined` writes unconditionally.
-   * @param scope - the named scope whose section to write; the global section when omitted.
-   * @returns the namespace's redacted view under that scope after the write.
+   * @returns the namespace's redacted view after the write.
    * @throws RemoteError when the request is invalid, no provider is mounted, or the provider refuses the write.
    */
   @Remote
@@ -200,9 +181,8 @@ export class SettingsController extends TypertRemoteService {
     ns: string,
     ops: SettingsPathOpView[],
     expectedRevision: number | undefined,
-    scope?: string,
   ): Promise<SettingsNamespaceView> {
-    return this.write(ns, 'mutate', ops, expectedRevision, scope)
+    return this.write(ns, 'mutate', ops, expectedRevision)
   }
 
   /**
@@ -282,24 +262,21 @@ export class SettingsController extends TypertRemoteService {
     mode: 'update' | 'replace' | 'mutate',
     input: Record<string, JsonValue> | SettingsPathOpView[],
     expectedRevision: number | undefined,
-    scope: string | undefined,
   ): Promise<SettingsNamespaceView> {
-    const parsed = settingsNamespaceRequestSchema.safeParse({ ns, ...scope === undefined ? {} : { scope } })
+    const parsed = settingsNamespaceRequestSchema.safeParse({ ns })
     if (!parsed.success) {
       throw new RemoteError('gateway/bad-request', `invalid payload for settings.${mode}`, { issues: parsed.error.issues })
     }
     const settings = this.provider()
     const namespace = parsed.data.ns
     try {
-      if (mode === 'update') await settings.update(namespace, input, expectedRevision, scope)
-      else if (mode === 'replace') await settings.replace(namespace, input, expectedRevision, scope)
-      else await settings.mutate(namespace, input as SettingsPathOp[], expectedRevision, scope)
+      if (mode === 'update') await settings.update(namespace, input, expectedRevision)
+      else if (mode === 'replace') await settings.replace(namespace, input, expectedRevision)
+      else await settings.mutate(namespace, input as SettingsPathOp[], expectedRevision)
     } catch (error: unknown) {
       throw rejected(ns, error)
     }
-    const descriptor = settings
-      .describe({ redactSecrets: true, ...scope === undefined ? {} : { scope } })
-      .find(candidate => candidate.ns === namespace)
+    const descriptor = settings.describe({ redactSecrets: true }).find(candidate => candidate.ns === namespace)
     if (descriptor === undefined) {
       // The write committed but the namespace vanished before this read: only a
       // concurrent registrant disposal can produce it.

+ 0 - 44
packages/api/settings-controller/tests/settings-controller.host.spec.ts

@@ -432,47 +432,3 @@ describe('the settings Remote namespace a configuration page calls', () => {
       .rejects.toMatchObject({ code: 'gateway/internal', message: 'path open failed: desktop unavailable' })
   })
 })
-
-describe('scoped reads and writes', () => {
-  it('describes one named scope from the kind, lists every registered scope, and refuses a malformed scope id', async () => {
-    const { controller, ctx } = await boot(MemorySettings, { doc: { [NS]: { preference: 'dark' } } })
-    const { createScope } = await import('@deepseek-ai/dsh-scope')
-    let scoped!: Awaited<ReturnType<typeof createScope>>
-    await ctx.plugin((host: Context) => { scoped = createScope(host, { id: 'standard' }, { id: 'preset/standard' }) })
-    await scoped.ctx.plugin({ inject: ['settings'], apply(inner: Context) { inner.settings.register(NS, Profile, { base: { preference: 'light' } }) } })
-
-    const global = controller.describe()
-    expect(global.scopes).toEqual(['preset/standard'])
-    expect(global.scope).toBeUndefined()
-    expect(global.namespaces[0]).toMatchObject({ ns: NS, registered: true, value: { preference: 'dark' } })
-
-    const standard = controller.describe('preset/standard')
-    expect(standard.scope).toBe('preset/standard')
-    expect(standard.namespaces[0]).toMatchObject({
-      ns: NS, scope: 'preset/standard', registered: true, base: { preference: 'light' }, inherited: { preference: 'dark' }, value: { preference: 'dark' },
-    })
-    const other = controller.describe('preset/other')
-    expect(other.namespaces[0]).toMatchObject({ ns: NS, scope: 'preset/other', registered: false, inherited: { preference: 'dark' } })
-    expect(other.namespaces[0]?.base).toBeUndefined()
-
-    const failure = await Promise.resolve().then(() => controller.describe('Not Valid')).catch((error: unknown) => error)
-    expect(remoteErrorOf(failure)).toMatchObject({ code: 'gateway/bad-request' })
-  })
-
-  it('writes one scope\'s section and answers with that scope\'s view', async () => {
-    const { controller, ctx } = await boot(MemorySettings, { doc: { [NS]: { preference: 'dark' } } })
-    const provider = ctx.get('settings') as MemorySettings
-
-    const updated = await controller.update(NS, { preference: 'light' }, undefined, 'preset/standard')
-    expect(updated).toMatchObject({ ns: NS, scope: 'preset/standard', registered: false, value: { preference: 'light' }, user: { preference: 'light' }, revision: 1 })
-    expect(provider.doc).toEqual({ [NS]: { preference: 'dark' }, scopes: { 'preset/standard': { [NS]: { preference: 'light' } } } })
-
-    const replaced = await controller.replace(NS, {}, 1, 'preset/standard')
-    expect(replaced).toMatchObject({ scope: 'preset/standard', value: { preference: 'dark' }, revision: 2 })
-    const mutated = await controller.mutate(NS, [{ op: 'set', path: ['preference'], value: 'light' }], undefined, 'preset/standard')
-    expect(mutated).toMatchObject({ scope: 'preset/standard', value: { preference: 'light' }, revision: 3 })
-
-    const failure = await controller.update(NS, {}, undefined, '').catch((error: unknown) => error)
-    expect(remoteErrorOf(failure)).toMatchObject({ code: 'gateway/bad-request' })
-  })
-})

+ 0 - 3
packages/api/settings-controller/tsconfig.json

@@ -26,9 +26,6 @@
     {
       "path": "../../util/native-command"
     },
-    {
-      "path": "../../core/scope"
-    },
     {
       "path": "../../settings/settings"
     },

+ 2 - 2
packages/boot/app-boot/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/boot/app-boot/README.md
-README.md: 94e46a03f5bd7603e8a5444e2663b937f028e65f
-README.zh.md: 516128a34ab8df8a96a4a9b7bfa5b12dba1b9112
+README.md: e070f187379638dff75e1bcfc3bfe835195d08c0
+README.zh.md: 9d56700ca22743c7fc484bba113b718751f935e0

+ 1 - 1
packages/boot/app-boot/README.md

@@ -65,7 +65,7 @@ The launcher provides `ctx.profileRuntime` before any configuration entry mounts
 <a id="patch-files"></a>
 ### Patch files
 
-Every layer above is a `cordis.patch.yml`: a top-level YAML sequence of the include plugin's `PatchOptions` — id-targeted overrides and `insert` lists — in the Loader's dialect, where `!!js` marks an expression the row's fiber evaluates. The `./patch-file` export is the one place that reads and writes such a file, so a file the boot accepts is a file the agent-preset roster and the plugin manager accept.
+Every layer above is a `cordis.patch.yml`: a top-level YAML sequence of the include plugin's `PatchOptions` — id-targeted overrides and `insert` lists — in the Loader's dialect, where `!!js` marks an expression the row's fiber evaluates. The `./patch-file` export is the one place that reads and writes such a file, so a file the boot accepts is a file the plugin manager accepts.
 
 Read a layer with `parsePatchList` (text in hand) or `readPatchListFile` (an absent file reads as `undefined`). Both anchor a relative `insert` row name such as `./plugin.js` to the file's own directory and fail loud on anything that is not a sequence of mappings, because a patch file that cannot be applied at all is a misconfiguration; a patch whose target row is absent stays a per-entry Loader warning.
 

+ 1 - 1
packages/boot/app-boot/README.zh.md

@@ -65,7 +65,7 @@ launcher 在任何配置行挂载前提供 `ctx.profileRuntime`。该运行时
 <a id="patch-files"></a>
 ### 补丁文件
 
-上面的每一层都是一个 `cordis.patch.yml`:一个顶层 YAML 序列,元素是 include 插件的 `PatchOptions`——按 id 定位的覆盖与 `insert` 列表——采用 Loader 的方言,其中 `!!js` 标记一个由该行 fiber 求值的表达式。`./patch-file` 导出是读写这种文件的唯一地方,因此启动接受的文件就是 agent preset roster 与插件管理器接受的文件。
+上面的每一层都是一个 `cordis.patch.yml`:一个顶层 YAML 序列,元素是 include 插件的 `PatchOptions`——按 id 定位的覆盖与 `insert` 列表——采用 Loader 的方言,其中 `!!js` 标记一个由该行 fiber 求值的表达式。`./patch-file` 导出是读写这种文件的唯一地方,因此启动接受的文件就是 插件管理器接受的文件。
 
 用 `parsePatchList`(已有文本)或 `readPatchListFile`(文件不存在时读到 `undefined`)读取一层。两者都把 `insert` 行中相对的名字(如 `./plugin.js`)锚定到文件自己的目录,并对任何不是"映射序列"的内容直接报错,因为一个完全无法施加的补丁文件就是配置错误;而目标行不存在的单条补丁仍然只是 Loader 的逐条警告。
 

+ 2 - 2
packages/boot/plugin-manager/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/boot/plugin-manager/README.md
-README.md: 2ea3709d441ca9327048ffe0e001acf08020c33d
-README.zh.md: 6c0cc3d17f67f96ab41cb6e3625f1cfa536671a9
+README.md: 538f837dbaaec0b9b89d0ba2a70a44d5f67cf32f
+README.zh.md: 803dbd572b735f95f828260290fd7d39dd4e6fbe

+ 5 - 9
packages/boot/plugin-manager/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`dsh-plugin-manager` installs packages and manages their declared plugin rows through the CLI and Web host. Installation reads metadata without executing modules. The manager enables whole bundle layers, edits global or preset user patches, retries failures, and reports current runtime issues. Failures carry `plugins/*` codes; the [Host adapter](../../host/plugin-manager/README.md) exposes the operations through Remote.
+`dsh-plugin-manager` installs packages and manages their declared plugin rows through the CLI and Web host. Installation reads metadata without executing modules. The manager enables whole bundle layers, edits the profile user patch, retries failures, and reports current runtime issues. Failures carry `plugins/*` codes; the [Host adapter](../../host/plugin-manager/README.md) exposes the operations through Remote.
 
 ## Table of Contents
 
@@ -51,12 +51,11 @@ console.log(outcome.installed, outcome.removed)
 
 ### Managing the booted profile
 
-Build a `PluginManager` over the Cordis context and readers for what it needs per call — the profile runtime, the preset roster's layers, and the running-agent count — so a composition that gains or lacks one of them is answered at call time rather than at mount:
+Build a `PluginManager` over the Cordis context and readers for what it needs per call — the profile runtime and the running-agent count — so a composition that gains or lacks one of them is answered at call time rather than at mount:
 
 ```ts
 import type { Context } from '@deepseek-ai/cordis'
 import type {} from '@deepseek-ai/dsh-agent'
-import type {} from '@deepseek-ai/dsh-agent-presets'
 import type {} from '@deepseek-ai/dsh-app-boot'
 import { PluginManager, type PluginToolingConfig } from '@deepseek-ai/dsh-plugin-manager'
 
@@ -66,7 +65,6 @@ declare const config: PluginToolingConfig
 const manager = new PluginManager(ctx, {
   config,
   runtime: () => ctx.get('profileRuntime'),
-  presets: () => ctx.get('agentPresets'),
   runningAgents: () => (ctx.get('agents')?.list() ?? []).filter(agent => agent.status === 'running').length,
 })
 console.log(await manager.list())
@@ -76,13 +74,13 @@ console.log(await manager.list())
 
 `enable` selects the whole bundle layer and recomposes live profiles. Per-row failures retain the enabled choice and successful siblings; results report `issues`, and the list can show `partial` or `failed`. Preparation failures revert the enable selection and raise `plugins/enable-failed`. `disable` removes the whole layer, including overrides. `retry` disables, awaits cleanup, and enables again. Startup-only profiles report `effect: restart`. Unapplied selections on live profiles report `failed`, preserving the rows actually running; `restart-required` is reserved for startup-only profiles. `uninstall` disables the bundle, removes user-inserted references, and runs pnpm remove.
 
-`addRow` writes an explicitly declared `dsh.plugins` module to the profile’s global `cordis.patch.yml` or a preset user layer. It preserves declared defaults, checks the target row id, and performs no pre-mount import. `removeRow` removes a user insert. `setRowDisabled` writes or removes `disabled: true`, preserving the bundle’s own condition. Global edits recompose immediately on live profiles; preset edits apply to subsequent generations. `dependents` reports injection dependents and user-layer module references.
+`addRow` writes an explicitly declared `dsh.plugins` module to the profile’s global `cordis.patch.yml`. It preserves declared defaults, checks the target row id, and performs no pre-mount import. `removeRow` removes a user insert. `setRowDisabled` writes or removes `disabled: true`, preserving the bundle’s own condition. Global edits recompose immediately on live profiles. `dependents` reports injection dependents and user-layer module references.
 
 The manager runs one mutation at a time — a second call while one runs fails with `plugins/busy` naming the operation in flight — and `add` and `uninstall` refuse to change `node_modules` while a session is running, with `plugins/agents-running`. Every change is followed by a `plugins/changed` event on the context, and an install run emits pnpm's output as `plugins/install-log` chunks, each naming the command line it ran and the profile directory it ran in, and, with colour on, carrying pnpm's SGR escapes.
 
 ### Failures
 
-Every refusal or failure is a `PluginOperationError` with a stable `code` and `details` typed by it: `plugins/unavailable` (no profile runtime, or no roster for a preset target), `plugins/not-installed`, `plugins/not-enableable`, `plugins/enable-failed`, `plugins/install-failed`, `plugins/row-conflict`, `plugins/busy`, `plugins/agents-running`, and `plugins/bad-request` for a request that names nothing the profile has. `pluginOperationFailureOf` narrows a caught value to the code-discriminated union.
+Every refusal or failure is a `PluginOperationError` with a stable `code` and `details` typed by it: `plugins/unavailable` (no profile runtime), `plugins/not-installed`, `plugins/not-enableable`, `plugins/enable-failed`, `plugins/install-failed`, `plugins/row-conflict`, `plugins/busy`, `plugins/agents-running`, and `plugins/bad-request` for a request that names nothing the profile has. `pluginOperationFailureOf` narrows a caught value to the code-discriminated union.
 
 -----
 
@@ -106,7 +104,7 @@ Retry removes the entire layer and awaits removed-fiber cleanup before adding it
 
 ### What the manager reads and what it is handed
 
-The manager reads Loader entries, the reflect store and entry-owned diagnostics. Profile, preset and agent facts arrive through per-call readers. The [Host adapter](../../host/plugin-manager/README.md) converts Loader lifecycle changes into `plugins/changed` notifications after settlement, including a pending row whose provider becomes available.
+The manager reads Loader entries, the reflect store and entry-owned diagnostics. Profile and agent facts arrive through per-call readers. The [Host adapter](../../host/plugin-manager/README.md) converts Loader lifecycle changes into `plugins/changed` notifications after settlement, including a pending row whose provider becomes available.
 
 ### Source map
 
@@ -133,7 +131,6 @@ Read these when the manager's contract is not enough: the runtime it drives, the
 
 - [App boot](../app-boot/README.md) — the profile runtime, external bundle isolation, and static package declarations.
 - [Patch files](../app-boot/README.md#patch-files) — how user-layer rows are read and written.
-- [Agent presets](../../preset/agent-presets/README.md) — the per-preset user layer a preset target writes.
 - [Host plugin manager](../../host/plugin-manager/README.md) — the `plugins` Remote over this manager.
 - [dsh app](../../../apps/cli/README.md) — the `dsh plugin` command over the installer.
 
@@ -157,7 +154,6 @@ These limits define what the manager will not do for a caller. They are current
 
 - **Updating a loaded package needs a restart** — Node caches ESM modules by URL and a hoisted install keeps the path; `pnpm update` through `add` rewrites the files but the running tree keeps the old modules until the process restarts.
 - **Dependents stop at injection** — a registry-type dependency (a tool, an LLM adapter) has no `inject` edge, so `dependents` cannot name a row that only reads what the package registered.
-- **A preset row is not composed live** — the manager writes the preset's layer; sessions created afterwards compose it, sessions already running keep their generation.
 - **No `engines.dsh` check yet** — the range is reported, not enforced against the running harness version.
 - **One process at a time** — the mutex is in-process and the patch-file writer takes a file lock, but the profile manifest has no lock: the CLI and a running Web host editing the same profile at once is not supported.
 

+ 5 - 9
packages/boot/plugin-manager/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-`dsh-plugin-manager` 通过 CLI 与 Web 宿主安装包并管理它们声明的插件行。安装读取元信息,不执行模块。管理器启停整份组合包层,编辑全局或预设用户 patch,重试失败并报告当前运行问题。失败携带 `plugins/*` 错误码;[Host 适配器](../../host/plugin-manager/README.zh.md) 通过 Remote 暴露这些操作。
+`dsh-plugin-manager` 通过 CLI 与 Web 宿主安装包并管理它们声明的插件行。安装读取元信息,不执行模块。管理器启停整份组合包层,编辑profile 用户 patch,重试失败并报告当前运行问题。失败携带 `plugins/*` 错误码;[Host 适配器](../../host/plugin-manager/README.zh.md) 通过 Remote 暴露这些操作。
 
 ## 目录
 
@@ -51,12 +51,11 @@ console.log(outcome.installed, outcome.removed)
 
 ### 管理已启动的 profile
 
-在 Cordis 上下文之上构造 `PluginManager`,并交给它按调用读取所需之物的读取器——profile runtime、preset roster 的层、运行中的 agent 数——这样一个后来才有或始终没有其中之一的组合在调用时得到回答,而不是在挂载时:
+在 Cordis 上下文之上构造 `PluginManager`,并交给它按调用读取所需之物的读取器——profile runtime 和运行中的 agent 数——这样一个后来才有或始终没有其中之一的组合在调用时得到回答,而不是在挂载时:
 
 ```ts
 import type { Context } from '@deepseek-ai/cordis'
 import type {} from '@deepseek-ai/dsh-agent'
-import type {} from '@deepseek-ai/dsh-agent-presets'
 import type {} from '@deepseek-ai/dsh-app-boot'
 import { PluginManager, type PluginToolingConfig } from '@deepseek-ai/dsh-plugin-manager'
 
@@ -66,7 +65,6 @@ declare const config: PluginToolingConfig
 const manager = new PluginManager(ctx, {
   config,
   runtime: () => ctx.get('profileRuntime'),
-  presets: () => ctx.get('agentPresets'),
   runningAgents: () => (ctx.get('agents')?.list() ?? []).filter(agent => agent.status === 'running').length,
 })
 console.log(await manager.list())
@@ -76,13 +74,13 @@ console.log(await manager.list())
 
 `enable` 选择整份组合包层,并在实时 profile 中重组。逐行失败保留启用选择与成功的其他行;结果报告 `issues`,列表可显示 `partial` 或 `failed`。准备失败会撤销启用选择,并抛出 `plugins/enable-failed`。`disable` 移除整层,包括覆盖。`retry` 先禁用并等待清理,再启用。仅启动时生效的 profile 报告 `effect: restart`。实时 profile 中尚未应用的选择报告 `failed`,并保留实际运行的行;`restart-required` 只用于仅启动时生效的 profile。`uninstall` 禁用组合包、删除用户插入的引用、执行 pnpm remove。
 
-`addRow` 将显式声明的 `dsh.plugins` 模块写入 profile 的全局 `cordis.patch.yml` 或预设用户层。它保留声明的默认配置,检查目标行 id,不在挂载前 import。`removeRow` 删除用户插入。`setRowDisabled` 写入或删除 `disabled: true`,保留组合包自己的条件。全局编辑在实时 profile 中立即重组;预设编辑应用于后续代际。`dependents` 报告注入依赖方与用户层模块引用。
+`addRow` 将显式声明的 `dsh.plugins` 模块写入 profile 的全局 `cordis.patch.yml`。它保留声明的默认配置,检查目标行 id,不在挂载前 import。`removeRow` 删除用户插入。`setRowDisabled` 写入或删除 `disabled: true`,保留组合包自己的条件。全局编辑在实时 profile 中立即重组。`dependents` 报告注入依赖方与用户层模块引用。
 
 管理器一次只跑一个变更——上一个还在跑时再调用会以 `plugins/busy` 失败并点名正在进行的操作——`add` 与 `uninstall` 在有会话运行时拒绝改动 `node_modules`,报 `plugins/agents-running`。每次变更之后在上下文上发出 `plugins/changed` 事件,安装运行把 pnpm 的输出以 `plugins/install-log` 分块发出,每块都写明所跑的命令行与所在的 profile 目录,开了颜色时还带着 pnpm 的 SGR 转义。
 
 ### 失败
 
-每次拒绝或失败都是一个 `PluginOperationError`,带稳定的 `code` 与按码定型的 `details`:`plugins/unavailable`(没有 profile runtime,或 preset 目标没有 roster)、`plugins/not-installed`、`plugins/not-enableable`、`plugins/enable-failed`、`plugins/install-failed`、`plugins/row-conflict`、`plugins/busy`、`plugins/agents-running`,以及请求点名了 profile 没有的东西时的 `plugins/bad-request`。`pluginOperationFailureOf` 把捕获到的值收窄为按码区分的联合。
+每次拒绝或失败都是一个 `PluginOperationError`,带稳定的 `code` 与按码定型的 `details`:`plugins/unavailable`(没有 profile runtime)、`plugins/not-installed`、`plugins/not-enableable`、`plugins/enable-failed`、`plugins/install-failed`、`plugins/row-conflict`、`plugins/busy`、`plugins/agents-running`,以及请求点名了 profile 没有的东西时的 `plugins/bad-request`。`pluginOperationFailureOf` 把捕获到的值收窄为按码区分的联合。
 
 -----
 
@@ -106,7 +104,7 @@ subprocess seam 会清洗形似密钥的变量且没有 shell 模式,而 pnpm
 
 ### 管理器自己读什么,别人交给它什么
 
-管理器读取 Loader 条目、reflect 存储与条目自身诊断。profile、预设和 agent 信息通过逐次调用的读取器提供。[Host 适配器](../../host/plugin-manager/README.zh.md) 在状态稳定后将 Loader 生命周期变化转换为 `plugins/changed` 通知,包括等待中的行在提供方出现后恢复运行。
+管理器读取 Loader 条目、reflect 存储与条目自身诊断。profile 和 agent 信息通过逐次调用的读取器提供。[Host 适配器](../../host/plugin-manager/README.zh.md) 在状态稳定后将 Loader 生命周期变化转换为 `plugins/changed` 通知,包括等待中的行在提供方出现后恢复运行。
 
 ### 源码地图
 
@@ -133,7 +131,6 @@ subprocess seam 会清洗形似密钥的变量且没有 shell 模式,而 pnpm
 
 - [App boot](../app-boot/README.zh.md)——profile runtime、外部组合包隔离与静态包声明。
 - [补丁文件](../app-boot/README.zh.md#patch-files)——用户层的行如何读写。
-- [Agent presets](../../preset/agent-presets/README.zh.md)——preset 目标所写的每预设用户层。
 - [宿主插件管理器](../../host/plugin-manager/README.zh.md)——本管理器之上的 `plugins` Remote。
 - [dsh 应用](../../../apps/cli/README.zh.md)——安装器之上的 `dsh plugin` 命令。
 
@@ -157,7 +154,6 @@ subprocess seam 会清洗形似密钥的变量且没有 shell 模式,而 pnpm
 
 - **更新已加载的包需要重启**——Node 按 URL 缓存 ESM 模块,hoisted 安装下路径不变;经 `add` 做的 `pnpm update` 改写了文件,但运行中的树在进程重启前一直用旧模块。
 - **依赖检测止于注入**——注册型依赖(工具、LLM 适配器)没有 `inject` 边,因此 `dependents` 无法点名只读取该包所注册内容的行。
-- **preset 的行不在线组合**——管理器写入 preset 的层;之后创建的会话组合它,已在运行的会话保持其代际。
 - **尚无 `engines.dsh` 检查**——该范围只被报告,不对运行中的 harness 版本强制执行。
 - **一次只有一个进程**——互斥在进程内,补丁文件写入器持文件锁,但 profile manifest 没有锁:CLI 与运行中的 Web 宿主同时编辑同一个 profile 不受支持。
 

+ 1 - 1
packages/boot/plugin-manager/src/index.ts

@@ -20,4 +20,4 @@ export * from './errors.ts'
 export type * from './types.ts'
 export type { PluginToolingConfig, SpawnLike } from './helpers.ts'
 export { PluginInstaller, type PluginInstallerOptions, type PluginInstallOutcome } from './installer.ts'
-export { PluginManager, type PluginManagerOptions, type PresetLayers } from './manager.ts'
+export { PluginManager, type PluginManagerOptions } from './manager.ts'

+ 38 - 96
packages/boot/plugin-manager/src/manager.ts

@@ -1,7 +1,7 @@
 /**
  * Plugin management over the booted profile: every operation the `plugins`
  * Remote exposes, one mutation at a time, with the tree read through the
- * Cordis context and the profile runtime, the preset roster, and the
+ * Cordis context and the profile runtime and the
  * running-agent count read through per-call readers.
  * @module @deepseek-ai/dsh-plugin-manager/manager
  */
@@ -34,43 +34,15 @@ import type {
   PluginRowAddition,
   PluginRowIssue,
   PluginRowReference,
-  PluginRowTarget,
   PluginServiceDependent,
 } from './types.ts'
 import { ownedEntries, packageView } from './view.ts'
 
-/**
- * The agent-preset roster as row operations see it: where each preset's user
- * patch layer lives and which row ids its composition already carries. The
- * roster service satisfies this structurally; a host passes it in so this
- * package never imports the roster.
- */
-export interface PresetLayers {
-  /**
-   * The user patch layer file of one preset, created on the first write.
-   * @param presetId - the preset id.
-   * @returns the absolute path.
-   */
-  overlayPathFor(presetId: string): Promise<string>
-  /**
-   * Every preset with the path of its user patch layer when it has one.
-   * @returns the presets.
-   */
-  list(): Promise<readonly { readonly id: string; readonly overlayPath?: string }[]>
-  /**
-   * Every preset's composition as rows, under the ids the composition declares; an anonymous row has none.
-   * @returns one composition per preset.
-   */
-  compositionInventory(): Promise<readonly { readonly id: string; readonly rows: readonly { readonly entryId: string | null }[] }[]>
-}
-
 /** What {@link PluginManager} needs beyond the Cordis context it reads the tree through. */
 export interface PluginManagerOptions {
   readonly config: PluginToolingConfig
   /** The booted profile, read per call; `undefined` means no profile is composed in this process. */
   readonly runtime: () => ProfileRuntime | undefined
-  /** The preset roster's layers, read per call; `undefined` when no roster is composed. */
-  readonly presets: () => PresetLayers | undefined
   /** How many agents are running; `add` and `uninstall` refuse while any is. */
   readonly runningAgents: () => number
   /** Test seam: the child spawner; defaults to `node:child_process`. */
@@ -82,7 +54,7 @@ export interface PluginManagerOptions {
 /**
  * Plugin management over the booted profile: every operation the `plugins`
  * Remote exposes, with the tree read through the Cordis context and the
- * profile runtime, the preset roster, and the running-agent count read
+ * profile runtime and the running-agent count read
  * through {@link PluginManagerOptions} per call.
  *
  * Every write touches the profile manifest, the user layers, or
@@ -96,7 +68,7 @@ export class PluginManager {
 
   /**
    * @param ctx - the context whose Loader tree and services the operations read.
-   * @param options - the profile runtime, the roster, the agent count, and the tooling bounds.
+   * @param options - the profile runtime, the agent count, and the tooling bounds.
    */
   constructor(private readonly ctx: Context, private readonly options: PluginManagerOptions) {}
 
@@ -238,10 +210,10 @@ export class PluginManager {
       await this.disableNow(packageName)
     }
     for (const reference of references) {
-      await this.editLayer(runtime, reference.target, (document) => { document.removeInsert(reference.rowId) })
+      await this.editLayer(runtime, (document) => { document.removeInsert(reference.rowId) })
     }
     await installer.remove(packageName)
-    if (references.some(reference => reference.target.kind === 'global') && runtime.patchReload === 'live') {
+    if (references.length > 0 && runtime.patchReload === 'live') {
       await runtime.recompose()
     }
     this.changed('uninstall', packageName)
@@ -357,28 +329,23 @@ export class PluginManager {
   }
 
   /**
-   * Add a row naming one of the package's modules to a user layer: the
-   * profile's global `cordis.patch.yml`, or an agent preset's.
+   * Add a row naming one of the package's modules to the profile's `cordis.patch.yml`.
    * @param packageName - the installed package.
-   * @param target - which layer.
    * @param options - `module` selects a declared `dsh.plugins[]` name (default `.`),
    * `id` overrides the derived row id, `config` overrides the declared default.
    * @returns where the row landed.
    * @throws {PluginOperationError} `plugins/not-installed`, `plugins/not-enableable`
-   * when the module is absent from `dsh.plugins`, `plugins/row-conflict`,
-   * or `plugins/unavailable` for a preset target without a roster.
+   * when the module is absent from `dsh.plugins`, or `plugins/row-conflict`.
    */
   async addRow(
     packageName: string,
-    target: PluginRowTarget,
     options?: { module?: string; id?: string; config?: JsonValue },
   ): Promise<PluginRowAddition> {
-    return this.exclusive('addRow', packageName, () => this.addRowNow(packageName, target, options))
+    return this.exclusive('addRow', packageName, () => this.addRowNow(packageName, options))
   }
 
   private async addRowNow(
     packageName: string,
-    target: PluginRowTarget,
     options?: { module?: string; id?: string; config?: JsonValue },
   ): Promise<PluginRowAddition> {
     const runtime = this.runtime()
@@ -398,30 +365,29 @@ export class PluginManager {
     }
     const config = options?.config !== undefined ? options.config : addable.config !== undefined ? addable.config : {}
     const row: PatchRow = { id: rowId, name: moduleName, config }
-    if (await this.rowExists(runtime, target, rowId)) {
-      throw new PluginOperationError('plugins/row-conflict', `${NAME}: a row ${JSON.stringify(rowId)} already exists`, { rowId, target })
+    if (await this.rowExists(runtime, rowId)) {
+      throw new PluginOperationError('plugins/row-conflict', `${NAME}: a row ${JSON.stringify(rowId)} already exists`, { rowId })
     }
-    const file = await this.editLayer(runtime, target, (document) => { document.appendInsert(row) })
+    const file = await this.editLayer(runtime, (document) => { document.appendInsert(row) })
     this.changed('row', packageName)
-    return { target, rowId, file }
+    return { rowId, file }
   }
 
   /**
    * Remove a row a user layer inserted.
-   * @param target - which layer.
    * @param rowId - the inserted row's id.
    * @throws {PluginOperationError} `plugins/bad-request` when the layer inserts no such row.
    */
-  async removeRow(target: PluginRowTarget, rowId: string): Promise<void> {
-    return this.exclusive('removeRow', rowId, () => this.removeRowNow(target, rowId))
+  async removeRow(rowId: string): Promise<void> {
+    return this.exclusive('removeRow', rowId, () => this.removeRowNow(rowId))
   }
 
-  private async removeRowNow(target: PluginRowTarget, rowId: string): Promise<void> {
+  private async removeRowNow(rowId: string): Promise<void> {
     const runtime = this.runtime()
     // A holder rather than a `let`: the assignment happens inside the edit
     // callback, which control-flow narrowing does not see.
     const outcome = { removed: false }
-    await this.editLayer(runtime, target, (document) => { outcome.removed = document.removeInsert(rowId) })
+    await this.editLayer(runtime, (document) => { outcome.removed = document.removeInsert(rowId) })
     if (!outcome.removed) {
       throw new PluginOperationError('plugins/bad-request', `${NAME}: the layer inserts no row ${JSON.stringify(rowId)}`, {})
     }
@@ -432,17 +398,16 @@ export class PluginManager {
    * Switch one row off or on in a user layer. Deny-only: `true` writes
    * `disabled: true` for the row, `false` removes that key, so a bundle's
    * own `!!js` gate is restored rather than overridden.
-   * @param target - which layer.
    * @param rowId - the row's id as the composition declares it.
    * @param disabled - whether the layer should switch the row off.
    */
-  async setRowDisabled(target: PluginRowTarget, rowId: string, disabled: boolean): Promise<void> {
-    return this.exclusive('setRowDisabled', rowId, () => this.setRowDisabledNow(target, rowId, disabled))
+  async setRowDisabled(rowId: string, disabled: boolean): Promise<void> {
+    return this.exclusive('setRowDisabled', rowId, () => this.setRowDisabledNow(rowId, disabled))
   }
 
-  private async setRowDisabledNow(target: PluginRowTarget, rowId: string, disabled: boolean): Promise<void> {
+  private async setRowDisabledNow(rowId: string, disabled: boolean): Promise<void> {
     const runtime = this.runtime()
-    await this.editLayer(runtime, target, (document) => {
+    await this.editLayer(runtime, (document) => {
       if (disabled) document.setRowField(rowId, 'disabled', true)
       else document.deleteRowField(rowId, 'disabled')
     })
@@ -482,70 +447,47 @@ export class PluginManager {
   /** Every user-layer row naming the package or one of its subpaths. */
   private async rowReferences(runtime: ProfileRuntime, packageName: string): Promise<PluginRowReference[]> {
     const found: PluginRowReference[] = []
-    const names = (target: PluginRowTarget, rows: readonly PatchRow[]): void => {
+    const names = (rows: readonly PatchRow[]): void => {
       for (const row of rows) {
         // A group's config is a row list once the patch parser accepted the file.
         /* v8 ignore next */
-        if (row.group === true) names(target, (row.config ?? []) as PatchRow[])
+        if (row.group === true) names((row.config ?? []) as PatchRow[])
         if (typeof row.id !== 'string') continue
         if (row.name === packageName || row.name.startsWith(`${packageName}/`)) {
-          found.push({ target, rowId: row.id, moduleName: row.name })
+          found.push({ rowId: row.id, moduleName: row.name })
         }
       }
     }
-    const scan = async (target: PluginRowTarget, file: string): Promise<void> => {
-      let patches
-      try {
-        patches = await readPatchListFile(NAME, file, 'patches')
-      } catch {
-        // An unreadable layer names nothing this call can act on; the
-        // launcher and the roster report it as their own failure.
-        return
-      }
-      for (const patch of patches ?? []) if (patch.insert !== undefined) names(target, patch.insert)
-    }
-    await scan({ kind: 'global' }, runtime.patchPath)
-    const presets = this.options.presets()
-    if (presets !== undefined) {
-      for (const preset of await presets.list()) {
-        if (preset.overlayPath !== undefined) await scan({ kind: 'preset', preset: preset.id }, preset.overlayPath)
-      }
+    let patches
+    try {
+      patches = await readPatchListFile(NAME, runtime.patchPath, 'patches')
+    } catch {
+      // An unreadable layer names nothing this call can act on; the
+      // launcher reports it as a composition failure.
+      return found
     }
+    for (const patch of patches ?? []) if (patch.insert !== undefined) names(patch.insert)
     return found
   }
 
-  /** Whether the target layer's composition already carries a row with `rowId`. */
-  private async rowExists(runtime: ProfileRuntime, target: PluginRowTarget, rowId: string): Promise<boolean> {
-    if (target.kind === 'global') {
-      const tree = rootIncludeEntry(this.ctx.root)?.subtree
-      return [...this.ctx.loader.entries()].some(entry => entry.parent.tree === tree && entry.options.id === rowId)
-        || (await readPatchListFile(NAME, runtime.patchPath, 'patches') ?? []).some(patch => patch.insert?.some(row => row.id === rowId))
-    }
-    const composition = (await this.presets().compositionInventory()).find(candidate => candidate.id === target.preset)
-    return composition?.rows.some(row => row.entryId === rowId) ?? false
-  }
-
-  /** The roster's layers, or the failure a preset target without a roster receives. */
-  private presets(): PresetLayers {
-    const presets = this.options.presets()
-    if (presets === undefined) {
-      throw new PluginOperationError('plugins/unavailable', `${NAME}: no agent-preset roster is composed`, { reason: 'no roster' })
-    }
-    return presets
+  /** Whether the profile's composition already carries a row with `rowId`. */
+  private async rowExists(runtime: ProfileRuntime, rowId: string): Promise<boolean> {
+    const tree = rootIncludeEntry(this.ctx.root)?.subtree
+    return [...this.ctx.loader.entries()].some(entry => entry.parent.tree === tree && entry.options.id === rowId)
+      || (await readPatchListFile(NAME, runtime.patchPath, 'patches') ?? []).some(patch => patch.insert?.some(row => row.id === rowId))
   }
 
   /** Edit one user layer file and, for the live global layer, recompose. */
   private async editLayer(
     runtime: ProfileRuntime,
-    target: PluginRowTarget,
     mutate: Parameters<typeof mutatePatchFile>[1],
   ): Promise<string> {
-    const file = target.kind === 'global' ? runtime.patchPath : await this.presets().overlayPathFor(target.preset)
+    const file = runtime.patchPath
     await mutatePatchFile(file, mutate, { binName: NAME, mode: 0o600, dirMode: 0o700 })
     // The profile launcher's watcher reapplies the global file on its own;
     // recomposing here makes the change visible to this call's caller before
     // it returns, and the watcher's later pass composes the same text.
-    if (target.kind === 'global' && runtime.patchReload === 'live') await runtime.recompose()
+    if (runtime.patchReload === 'live') await runtime.recompose()
     return file
   }
 

+ 3 - 10
packages/boot/plugin-manager/src/types.ts

@@ -51,7 +51,7 @@ export interface PluginPackageRowView {
   readonly failure?: { readonly stage: string; readonly message: string }
 }
 
-/** One agent-plane module a package declares addable to a composition. */
+/** One module a package declares addable to the profile. */
 export interface PluginPackageAddableView {
   /** The module as it is named in a row: the bare package for `.`, else `<package>/<subpath>`. */
   readonly moduleName: string
@@ -92,17 +92,12 @@ export interface PluginPackageView {
   readonly issues?: readonly PluginRowIssue[]
   /** Ids of built-in rows the bundle's patch overrides. */
   readonly overrides: readonly string[]
-  /** Agent-plane modules the package declares addable. */
+  /** Modules the package declares addable. */
   readonly addable: readonly PluginPackageAddableView[]
   /** Whether the profile applies user patch files while running; false means changes wait for a restart. */
   readonly liveReload: boolean
 }
 
-/** Where a composition row is added or edited: the profile's global user layer, or one preset's. */
-export type PluginRowTarget =
-  | { readonly kind: 'global' }
-  | { readonly kind: 'preset'; readonly preset: string }
-
 /** A package pnpm installed that the run removed again, with the check it failed. */
 export interface PluginInstallRejection {
   /** The package name. */
@@ -157,7 +152,6 @@ export interface PluginServiceDependent {
 
 /** One user-layer row that names a module of the package. */
 export interface PluginRowReference {
-  readonly target: PluginRowTarget
   readonly rowId: string
   readonly moduleName: string
 }
@@ -170,7 +164,6 @@ export interface PluginDependents {
 
 /** Where a row was added. */
 export interface PluginRowAddition {
-  readonly target: PluginRowTarget
   /** The id the row was given. */
   readonly rowId: string
   /** The user layer file the row was written to. */
@@ -214,7 +207,7 @@ export interface PluginOperationDetailsMap {
   /** pnpm exited non-zero, could not be spawned, or timed out. */
   'plugins/install-failed': { readonly spec: string; readonly exitCode: number | null; readonly log: string }
   /** The row id is already taken in the target user layer. */
-  'plugins/row-conflict': { readonly rowId: string; readonly target: PluginRowTarget }
+  'plugins/row-conflict': { readonly rowId: string }
   /** Another mutation is still running; the manager runs one at a time and refuses rather than queues. */
   'plugins/busy': {
     readonly operation: string

+ 25 - 59
packages/boot/plugin-manager/tests/plugin-manager.spec.ts

@@ -4,7 +4,7 @@
  * through `boot()` with the profile runtime the launcher provides, and a
  * fake pnpm that edits the profile the way the real one does. The manager
  * is built the way the Web host's adapter builds it, reading the runtime,
- * the roster, and the agent registry off the context per call.
+ * the agent registry off the context per call.
  */
 
 import { EventEmitter } from 'node:events'
@@ -26,7 +26,6 @@ import {
   type PluginInstallLogChunk, type PluginToolingConfig, type SpawnLike,
 } from '@deepseek-ai/dsh-plugin-manager'
 import type {} from '@deepseek-ai/dsh-agent'
-import type {} from '@deepseek-ai/dsh-agent-presets'
 
 const NAME = 'dsh-test'
 
@@ -233,12 +232,11 @@ async function bootProfile(staged: StagedHome, internals: Internals = {}, config
   return { ctx, manager, runtime: ctx.profileRuntime, changes, log }
 }
 
-/** The manager as the Web host's adapter builds it: runtime, roster, and agent count read off the context per call. */
+/** The manager as the Web host's adapter builds it: runtime and agent count read off the context per call. */
 function managerOver(ctx: Context, internals: Internals = {}, config: Partial<PluginToolingConfig> = {}): PluginManager {
   return new PluginManager(ctx, {
     config: managerConfig(config),
     runtime: () => ctx.get('profileRuntime'),
-    presets: () => ctx.get('agentPresets'),
     runningAgents: () => (ctx.get('agents')?.list() ?? []).filter(agent => agent.status === 'running').length,
     ...internals,
   })
@@ -348,12 +346,12 @@ describe('PluginManager', () => {
     expect(views.find(view => view.name === 'declared')?.addable[0]).toMatchObject({ moduleName: 'declared', config: { custom: 1 } })
     expect(existsSync(marker)).toBe(false)
     for (const id of ['', 'nested:child']) {
-      await expect(manager.addRow('declared', { kind: 'global' }, { id })).rejects.toMatchObject({ code: 'plugins/bad-request' })
+      await expect(manager.addRow('declared', { id })).rejects.toMatchObject({ code: 'plugins/bad-request' })
     }
-    const added = await manager.addRow('declared', { kind: 'global' }, { config: null })
+    const added = await manager.addRow('declared', { config: null })
     expect(readFileSync(added.file, 'utf8')).toContain('config: null')
     expect(views.find(view => view.name === 'unknown')?.kind).toBe('unknown')
-    await expect(manager.addRow('unknown', { kind: 'global' })).rejects.toMatchObject({ code: 'plugins/not-enableable' })
+    await expect(manager.addRow('unknown')).rejects.toMatchObject({ code: 'plugins/not-enableable' })
   })
 
   it('reports plugins/unavailable without a profile runtime', async () => {
@@ -543,7 +541,7 @@ describe('PluginManager', () => {
       const { manager } = await bootProfile(staged)
       await manager.enable('ext-off')
       await manager.enable('ext-waiting')
-      await manager.setRowDisabled({ kind: 'global' }, 'b', true)
+      await manager.setRowDisabled('b', true)
 
       const views = await manager.list()
 
@@ -553,7 +551,7 @@ describe('PluginManager', () => {
         ['a', false, 'composition', null],
         ['b', false, 'user', null],
       ])
-      await manager.setRowDisabled({ kind: 'global' }, 'b', true)
+      await manager.setRowDisabled('b', true)
       const waiting = views.find(view => view.name === 'ext-waiting')
       expect(waiting).toMatchObject({ status: 'failed', reason: expect.stringContaining('fixtureSvc') as string })
       expect(waiting?.rows).toEqual([expect.objectContaining({ entryId: 'include:w', phase: 'pending', failure: expect.objectContaining({ stage: 'inject-pending' }) as object })])
@@ -906,24 +904,24 @@ describe('PluginManager', () => {
       addDependency(staged.profileDir, '@acme/ext-plugin')
       const { ctx, manager, changes } = await bootProfile(staged)
 
-      const added = await manager.addRow('@acme/ext-plugin', { kind: 'global' })
+      const added = await manager.addRow('@acme/ext-plugin')
 
-      expect(added).toEqual({ target: { kind: 'global' }, rowId: 'acme/ext-plugin', file: join(staged.profileDir, 'cordis.patch.yml') })
+      expect(added).toEqual({ rowId: 'acme/ext-plugin', file: join(staged.profileDir, 'cordis.patch.yml') })
       expect(readFileSync(added.file, 'utf8')).toBe('- insert:\n    - id: acme/ext-plugin\n      name: "@acme/ext-plugin"\n      config: {}\n')
       // Composed live: the row is in the tree, though its module cannot import from the temp home.
       expect(entryIds(ctx)).toContain('include:acme/ext-plugin')
-      await expect(manager.addRow('@acme/ext-plugin', { kind: 'global' })).rejects.toMatchObject({ code: 'plugins/row-conflict' })
+      await expect(manager.addRow('@acme/ext-plugin')).rejects.toMatchObject({ code: 'plugins/row-conflict' })
 
-      await manager.setRowDisabled({ kind: 'global' }, 'acme/ext-plugin', true)
+      await manager.setRowDisabled('acme/ext-plugin', true)
       expect(ctx.loader.resolve('include:acme/ext-plugin')?.disabled).toBe(true)
       expect(readFileSync(added.file, 'utf8')).toContain('- id: acme/ext-plugin\n  disabled: true\n')
-      await manager.setRowDisabled({ kind: 'global' }, 'acme/ext-plugin', false)
+      await manager.setRowDisabled('acme/ext-plugin', false)
       expect(ctx.loader.resolve('include:acme/ext-plugin')?.disabled).toBe(false)
       expect(readFileSync(added.file, 'utf8')).not.toContain('disabled')
 
-      await manager.removeRow({ kind: 'global' }, 'acme/ext-plugin')
+      await manager.removeRow('acme/ext-plugin')
       expect(entryIds(ctx)).not.toContain('include:acme/ext-plugin')
-      await expect(manager.removeRow({ kind: 'global' }, 'acme/ext-plugin')).rejects.toMatchObject({ code: 'plugins/bad-request' })
+      await expect(manager.removeRow('acme/ext-plugin')).rejects.toMatchObject({ code: 'plugins/bad-request' })
       expect(changes.map(change => change.reason)).toEqual(['row', 'row', 'row', 'row'])
     })
 
@@ -933,10 +931,10 @@ describe('PluginManager', () => {
       addDependency(staged.profileDir, 'ext-plugin')
       const { ctx, manager } = await bootProfile(staged)
 
-      await manager.addRow('ext-plugin', { kind: 'global' })
+      await manager.addRow('ext-plugin')
 
       expect(entryIds(ctx)).not.toContain('include:ext-plugin')
-      await expect(manager.addRow('ext-plugin', { kind: 'global' })).rejects.toMatchObject({ code: 'plugins/row-conflict' })
+      await expect(manager.addRow('ext-plugin')).rejects.toMatchObject({ code: 'plugins/row-conflict' })
     })
 
     it('adds a declared addable module with its default config and an explicit id, and refuses the rest', async () => {
@@ -952,47 +950,15 @@ describe('PluginManager', () => {
       addDependency(staged.profileDir, 'ext-bundle')
       const { manager } = await bootProfile(staged)
 
-      const added = await manager.addRow('ext-bundle', { kind: 'global' }, { module: './tools/sql.js', id: 'sql' })
+      const added = await manager.addRow('ext-bundle', { module: './tools/sql.js', id: 'sql' })
       expect(added.rowId).toBe('sql')
       expect(readFileSync(added.file, 'utf8')).toContain('- id: sql\n      name: ext-bundle/tools/sql.js\n      config:\n        dsn: sqlite://\n')
-      await expect(manager.addRow('ext-bundle', { kind: 'global' })).rejects.toMatchObject({ code: 'plugins/not-enableable' })
-      await expect(manager.addRow('ext-bundle', { kind: 'global' }, { module: './missing.js' })).resolves.toMatchObject({ rowId: 'ext-bundle/missing.js' })
-      await expect(manager.addRow('absent', { kind: 'global' })).rejects.toMatchObject({ code: 'plugins/not-installed' })
+      await expect(manager.addRow('ext-bundle')).rejects.toMatchObject({ code: 'plugins/not-enableable' })
+      await expect(manager.addRow('ext-bundle', { module: './missing.js' })).resolves.toMatchObject({ rowId: 'ext-bundle/missing.js' })
+      await expect(manager.addRow('absent')).rejects.toMatchObject({ code: 'plugins/not-installed' })
     })
 
-    it('writes a preset\'s layer through the roster, and refuses a preset target without one', async () => {
-      const staged = await stageHome()
-      stagePackage(staged.profileDir, 'ext-plugin', { plugins: [{ name: '.' }], main: 'export const name = "p"\nexport function apply() {}\n' })
-      addDependency(staged.profileDir, 'ext-plugin')
-      const { ctx, manager } = await bootProfile(staged)
-      await expect(manager.addRow('ext-plugin', { kind: 'preset', preset: 'standard' })).rejects.toMatchObject({ code: 'plugins/unavailable' })
 
-      const overlay = join(staged.home, '.agent-presets', 'standard', 'cordis.patch.yml')
-      const roster = {
-        overlayPathFor: (id: string) => Promise.resolve(join(staged.home, '.agent-presets', id, 'cordis.patch.yml')),
-        compositionInventory: () => Promise.resolve([{ id: 'standard', rows: [{ entryId: 'tool-web' }] }]),
-        list: () => Promise.resolve([{ id: 'standard', overlayPath: existsSync(overlay) ? overlay : undefined }]),
-      }
-      ctx.provide('agentPresets', roster)
-      // No layer exists yet: nothing references the package.
-      expect((await manager.dependents('ext-plugin')).references).toEqual([])
-      // A preset the inventory does not know has no rows to conflict with.
-      const elsewhere = await manager.addRow('ext-plugin', { kind: 'preset', preset: 'other' })
-      expect(elsewhere.file).toBe(join(staged.home, '.agent-presets', 'other', 'cordis.patch.yml'))
-
-      const added = await manager.addRow('ext-plugin', { kind: 'preset', preset: 'standard' })
-      expect(added).toEqual({ target: { kind: 'preset', preset: 'standard' }, rowId: 'ext-plugin', file: overlay })
-      await expect(manager.addRow('ext-plugin', { kind: 'preset', preset: 'standard' }, { id: 'tool-web' })).rejects.toMatchObject({ code: 'plugins/row-conflict' })
-      await manager.setRowDisabled({ kind: 'preset', preset: 'standard' }, 'tool-web', true)
-      expect(readFileSync(overlay, 'utf8')).toBe('- insert:\n    - id: ext-plugin\n      name: ext-plugin\n      config: {}\n- id: tool-web\n  disabled: true\n')
-      // The preset's layer is not part of the host tree.
-      expect(entryIds(ctx)).not.toContain('include:ext-plugin')
-
-      const dependents = await manager.dependents('ext-plugin')
-      expect(dependents.references).toEqual([{ target: { kind: 'preset', preset: 'standard' }, rowId: 'ext-plugin', moduleName: 'ext-plugin' }])
-      await manager.removeRow({ kind: 'preset', preset: 'standard' }, 'ext-plugin')
-      expect(readFileSync(overlay, 'utf8')).toBe('- id: tool-web\n  disabled: true\n')
-    })
   })
 
   describe('dependents and uninstall', () => {
@@ -1021,14 +987,14 @@ describe('PluginManager', () => {
       await manager.enable('ext-provider')
       // A built-in row injecting the bundle's service, composed once the
       // provider is up (a boot would refuse a row left waiting).
-      writeFileSync(join(staged.profileDir, 'cordis.patch.yml'), `${readFileSync(join(staged.profileDir, 'cordis.patch.yml'), 'utf8')}- insert:\n    - id: needs-svc\n      name: cordis:consumer\n`)
+      writeFileSync(join(staged.profileDir, 'cordis.patch.yml'), `${readFileSync(join(staged.profileDir, 'cordis.patch.yml'), 'utf8')}- id: svc\n  disabled: false\n- insert:\n    - id: needs-svc\n      name: cordis:consumer\n`)
       await runtime.recompose()
 
       const dependents = await manager.dependents('ext-provider')
 
       expect(dependents.services).toEqual([{ service: 'fixtureSvc', providedBy: 'include:svc', injectedBy: ['include:needs-svc'] }])
       expect(dependents.references.map(reference => reference.rowId)).toEqual(['ref', 'nested-ref'])
-      expect(dependents.references[0]).toEqual({ target: { kind: 'global' }, rowId: 'ref', moduleName: 'ext-provider/tools/x.js' })
+      expect(dependents.references[0]).toEqual({ rowId: 'ref', moduleName: 'ext-provider/tools/x.js' })
     })
 
     it('ignores an unreadable user layer while collecting references', async () => {
@@ -1048,7 +1014,7 @@ describe('PluginManager', () => {
       const calls: string[][] = []
       const { ctx, manager, changes } = await bootProfile(staged, { spawn: recordingPnpm(staged.profileDir, calls) })
       await manager.enable('ext-bundle')
-      await manager.addRow('ext-bundle', { kind: 'global' }, { module: './extra.js' })
+      await manager.addRow('ext-bundle', { module: './extra.js' })
       expect(entryIds(ctx)).toEqual(expect.arrayContaining(['include:hello', 'include:ext-bundle/extra.js']))
 
       await manager.uninstall('ext-bundle')
@@ -1066,8 +1032,8 @@ describe('PluginManager', () => {
 
 describe('PluginOperationError', () => {
   it('carries its code and details, and is what pluginOperationFailureOf narrows to', () => {
-    const failure = new PluginOperationError('plugins/row-conflict', 'taken', { rowId: 'x', target: { kind: 'global' } })
-    expect(failure).toMatchObject({ name: 'PluginOperationError', code: 'plugins/row-conflict', message: 'taken', details: { rowId: 'x', target: { kind: 'global' } } })
+    const failure = new PluginOperationError('plugins/row-conflict', 'taken', { rowId: 'x' })
+    expect(failure).toMatchObject({ name: 'PluginOperationError', code: 'plugins/row-conflict', message: 'taken', details: { rowId: 'x' } })
     expect(pluginOperationFailureOf(failure)).toBe(failure)
     expect(pluginOperationFailureOf(new Error('plain'))).toBeUndefined()
   })

+ 2 - 2
packages/client/ui-agent-preset/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/client/ui-agent-preset/README.md
-README.md: b464d2749c51bdadb36dae6e494644b88d89930b
-README.zh.md: a552965889d85d9583b956113da46e51186088e3
+README.md: 476d0a351896835b4f92372c4a62854f2162a6e1
+README.zh.md: fa5a81b4af6299ae2253cf43809f07478d66885c

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 1
packages/client/ui-agent-preset/README.md


+ 2 - 2
packages/client/ui-agent-preset/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-使用本包可以为新的 Web GUI 会话选择 agent preset、查看会话当前的 preset,并在设置中管理 preset。你可以复制或删除 preset、选择默认值、打开其文件或进入详情页。Agent 模式选择器默认显示,也可以隐藏,而不改变运行中或历史会话。preset 在会话创建时固定;选择作用于新会话。没有 preset 提供方时,这些控件保持隐藏,每个会话都使用宿主组装。
+使用本包可以为新的 Web GUI 会话选择 agent preset、在会话标题中查看当前 preset,并在设置中管理可用 preset。Agent 模式选择器默认显示;设置可以隐藏它,而不会改变运行中或历史会话。preset 在会话创建时即固定,因此更改选择或默认值只影响此后创建的会话。如果部署未提供任何 preset,这些控件保持隐藏,每个会话都使用宿主组装。
 
 ## 目录
 
@@ -29,7 +29,7 @@ kind: "package-reference"
 
 ### 管理名单
 
-设置分区把名单呈现为卡片:复制对话框是创建 preset 的唯一入口——浏览器不编辑任何组装文本——每张自定义卡片都保留一个打开 preset 自身文件的位置动作。可见性开关只决定已保存的用户默认值是否生效:宿主在隐藏期间使用部署默认值,再次显示选择器时恢复已保存的默认值。选择器开启期间,选择健康且非默认的卡片会为后续会话写入新的用户默认值;如果当前新任务页已经复用一个空白会话,这次在设置中的明确选择也会通过既有选择链路把同一 preset 带到这个精确的空白会话。已开始及历史会话保持不变。保存期间开关会被禁用;写入失败时,界面保留先前的偏好并显示错误。隐藏选择器会禁用默认值选择与 Creator 启动,但名单查看、复制、位置和删除仍然可用。删除会移除 preset 目录,而已据其组装的会话继续运行。随附 preset 在只读查看器中打开,不提供位置或删除。每张可组装的卡片都带一个齿轮:打开该 preset 的详情页——返回名单的面包屑、名字、标记与描述,然后是注册进 `settings.agentPreset.detail` 的各段,每段拿到 preset 的 id 与显示名,由页面隔开;损坏的 preset 无可配置,其齿轮禁用;详情打开期间 preset 离开名单则回到卡片。名单行携带 `broken` 时渲染为标记卡片,其主体与复制均被禁用,因为损坏 preset 的副本只是另一个损坏 preset;损坏的自定义行保留位置与删除动作,以便修复文件、清掉幽灵目录。卡片正面仍显示 preset 自己的描述——在选择器里,一个包说明符不足以让人采取行动——宿主给出的原因作为工具提示附在徽标上,另有一个视觉隐藏的 alert 将该原因传达给辅助技术,而被禁用的卡片主体无法做到这一点。
+设置分区把名单呈现为卡片:复制对话框是创建 preset 的唯一入口——浏览器不编辑任何组装文本——每张自定义卡片都保留一个打开 preset 自身文件的位置动作。可见性开关只决定已保存的用户默认值是否生效:宿主在隐藏期间使用部署默认值,再次显示选择器时恢复已保存的默认值。选择器开启期间,选择健康且非默认的卡片会为后续会话写入新的用户默认值;如果当前新任务页已经复用一个空白会话,这次在设置中的明确选择也会通过既有选择链路把同一 preset 带到这个精确的空白会话。已开始及历史会话保持不变。保存期间开关会被禁用;写入失败时,界面保留先前的偏好并显示错误。隐藏选择器会禁用默认值选择与 Creator 启动,但名单查看、复制、位置和删除仍然可用。删除会移除 preset 目录,而已据其组装的会话继续运行。随附 preset 在只读查看器中打开,不提供位置或删除。名单行携带 `broken` 时渲染为标记卡片,其主体与复制均被禁用,因为损坏 preset 的副本只是另一个损坏 preset;损坏的自定义行保留位置与删除动作,以便修复文件、清掉幽灵目录。卡片正面仍显示 preset 自己的描述——在选择器里,一个包说明符不足以让人采取行动——宿主给出的原因作为工具提示附在徽标上,另有一个视觉隐藏的 alert 将该原因传达给辅助技术,而被禁用的卡片主体无法做到这一点。
 
 ### 对话式入口
 

+ 0 - 76
packages/client/ui-agent-preset/src/client/AgentPresetSection.module.css

@@ -496,79 +496,3 @@
   opacity: 0.4;
   cursor: default;
 }
-
-/* The detail page: a breadcrumb back to the roster, the preset's name and
-   marks, then the sections other plugins contribute. */
-.crumb {
-  display: inline-flex;
-  align-items: center;
-  gap: 6px;
-  border: 0;
-  padding: 0;
-  background: none;
-  color: var(--dsw-alias-label-tertiary);
-  font: inherit;
-  font-size: 13px;
-  line-height: 20px;
-  cursor: pointer;
-}
-
-.crumb:hover {
-  color: var(--dsw-alias-label-primary);
-}
-
-.crumb:focus-visible {
-  outline: 2px solid var(--dsw-alias-brand-primary);
-  outline-offset: 2px;
-}
-
-.crumbIcon {
-  transform: rotate(90deg);
-}
-
-.crumbSep::before {
-  content: '›';
-}
-
-.crumbHere {
-  color: var(--dsw-alias-label-primary);
-  font-weight: 600;
-}
-
-.detailHead {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  margin: 12px 0 4px;
-}
-
-.detailHead .inUse {
-  margin-left: 0;
-}
-
-.detailTitle {
-  margin: 0;
-  font-size: 20px;
-  line-height: 28px;
-  font-weight: 600;
-}
-
-.detailDesc {
-  margin: 0 0 20px;
-  max-width: 64ch;
-  color: var(--dsw-alias-label-secondary);
-  font-size: 14px;
-  line-height: 22px;
-}
-
-/* The sections other plugins contribute to the detail page, one under another. */
-.detailSections {
-  display: flex;
-  flex-direction: column;
-  gap: 18px;
-}
-
-.detailSections > * + * {
-  padding-top: 18px;
-  border-top: 0.5px solid var(--dsw-alias-border-l2);
-}

+ 3 - 46
packages/client/ui-agent-preset/src/client/AgentPresetSection.tsx

@@ -13,15 +13,13 @@
 import { useEffect, useLayoutEffect, useRef, useState } from 'react'
 import type { ReactNode } from 'react'
 import {
-  Button, IconBrowseOutline16, IconChevronDownOutline14, IconCopyOutline16, IconFolderOpenOutline16, IconPlusOutline16,
-  IconSettingsOutline16, IconTrashOutline16, Modal, Switch, Tag, Tooltip,
+  Button, IconBrowseOutline16, IconCopyOutline16, IconFolderOpenOutline16,
+  IconPlusOutline16, IconTrashOutline16, Modal, Switch, Tag, Tooltip,
 } from '@deepseek-ai/dsh-client-ui-primitives'
 import type { SnapshotStore } from '@deepseek-ai/dsh-client-store'
-import type { InjectFace, PropsLocale, PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
+import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
 import { draftBlocker, type AgentPresetSectionState } from './section-store.ts'
 import { presetDisplayText, type AgentPresetSettingsKey } from './locales.ts'
-// The detail page's child slot, declared by this section at registration.
-import type {} from './slot-contract.ts'
 import css from './AgentPresetSection.module.css'
 
 /** Registration-side business face for the management section. */
@@ -68,7 +66,6 @@ export interface AgentPresetSectionInjected {
 export type AgentPresetSectionProps =
   PropsRuntime<'settings.section'>
   & PropsLocale<'settings.agentPreset'>
-  & PropsRenderSlots<'settings.agentPreset.detail'>
   & InjectFace<AgentPresetSectionInjected>
 
 /** Copy-dialog sub-view props: the draft plus the actions that mutate it. */
@@ -184,10 +181,6 @@ function CardDescription({ text }: { text: string }): ReactNode {
 export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
   const { useAgentPresetSection, t, load } = props
   const state = useAgentPresetSection(snapshot => snapshot)
-  // The preset whose detail page is open; a preset that leaves the roster
-  // while open (deleted from its files) drops back to the cards.
-  const [openId, setOpenId] = useState<string | null>(null)
-  const openRow = openId === null ? undefined : state.rows.find(row => row.id === openId)
   const viewedId = state.view?.id
   const viewedRow = viewedId === undefined ? undefined : state.rows.find(row => row.id === viewedId)
   const viewedTitle = state.view === null
@@ -239,29 +232,6 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
     )
     : null
 
-  if (openRow !== undefined) {
-    const text = presetDisplayText(openRow, t)
-    return (
-      <div className={css.section} data-preset-detail={openRow.id}>
-        <button type="button" className={css.crumb} aria-label={t('backToRoster')} onClick={() => { setOpenId(null) }}>
-          <IconChevronDownOutline14 className={css.crumbIcon} aria-hidden="true" />
-          <span>{t('nav')}</span>
-          <span className={css.crumbSep} aria-hidden="true" />
-          <span className={css.crumbHere}>{text.name}</span>
-        </button>
-        <div className={css.detailHead}>
-          <h2 className={css.detailTitle}>{text.name}</h2>
-          <Tag>{openRow.trust === 'user' ? t('userTrust') : t('builtIn')}</Tag>
-          {openRow.isDefault ? <Tag tone="solid" className={css.inUse}>{t('inUse')}</Tag> : null}
-        </div>
-        <p className={css.detailDesc}>{text.description ?? t('noDescription')}</p>
-        <div className={css.detailSections}>
-          {props.renderSlot('settings.agentPreset.detail', { presetId: openRow.id, presetName: text.name })}
-        </div>
-      </div>
-    )
-  }
-
   return (
     <div className={css.section}>
       <h2 className={css.title}>{t('nav')}</h2>
@@ -382,19 +352,6 @@ export function AgentPresetSection(props: AgentPresetSectionProps): ReactNode {
                       <code className={css.cardId}>{row.id}</code>
                     </button>
                     <div className={css.cardFoot}>
-                      {/* The way into the preset's detail page — its
-                        capabilities and settings; a broken preset composes
-                        nothing, so there is nothing to configure. */}
-                      <button
-                        type="button"
-                        className={css.iconButton}
-                        disabled={row.broken !== undefined}
-                        data-tip={t('configure')}
-                        aria-label={`${t('configure')}: ${text.name}`}
-                        onClick={() => { setOpenId(row.id) }}
-                      >
-                        <IconSettingsOutline16 />
-                      </button>
                       {/* Shipped presets are the compositions a copy starts
                         from, so READING one is the point; a custom preset is
                         edited in its files instead, which the location action

+ 0 - 5
packages/client/ui-agent-preset/src/client/index.ts

@@ -43,7 +43,6 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
   }
 }
 
-export type { AgentPresetDetailOwnerProps } from './slot-contract.ts'
 export type { AgentPresetLabelInjected, AgentPresetLabelProps } from './AgentPresetLabel.tsx'
 export type { AgentPresetSeatInjected, AgentPresetSeatProps } from './AgentPresetSeat.tsx'
 export type { AgentPresetSectionInjected, AgentPresetSectionProps } from './AgentPresetSection.tsx'
@@ -218,9 +217,5 @@ export function apply(ctx: ClientContext): void {
     label: () => ctx.locale.bind('settings.agentPreset')('nav'),
     locale: 'settings.agentPreset',
     inject: sectionInjected,
-    // A preset's detail page is assembled from sections other plugins
-    // contribute (the plugin manager's capabilities list); the roster only
-    // names the preset.
-    children: { 'settings.agentPreset.detail': { kind: 'list', scope: 'root' } },
   }, AgentPresetSection))
 }

+ 0 - 5
packages/client/ui-agent-preset/src/client/locales.ts

@@ -17,7 +17,6 @@ export type AgentPresetSettingsKey =
   | 'openLocation' | 'showLocation' | 'revealedPathLabel'
   | 'idRequired' | 'idInvalid' | 'idTaken'
   | 'deleteTitle' | 'deleteDescription' | 'deleteConfirm' | 'deleting'
-  | 'configure' | 'backToRoster'
   | 'showPicker' | 'showPickerBeta' | 'showPickerDescription'
   | 'enablePickerToSetDefault' | 'enablePickerToCreate'
 
@@ -84,8 +83,6 @@ export const en: Record<AgentPresetSettingsKey, string> = {
     'The preset directory is deleted. Sessions already running on it keep working; new sessions cannot select it.',
   deleteConfirm: 'Delete',
   deleting: 'Deleting…',
-  configure: 'Configure',
-  backToRoster: 'Back to agent presets',
   showPicker: 'Allow switching Agent modes',
   showPickerBeta: 'Beta',
   showPickerDescription:
@@ -148,8 +145,6 @@ export const zh: Record<AgentPresetSettingsKey, string> = {
   deleteDescription: '预设目录将被删除。已在其上运行的会话不受影响;新会话将无法再选择它。',
   deleteConfirm: '删除',
   deleting: '正在删除…',
-  configure: '配置',
-  backToRoster: '返回 Agent 预设',
   showPicker: '允许切换agent模式',
   showPickerBeta: 'beta',
   showPickerDescription: '开启后,新任务可选择标准、PTC、创造、极简及自定义模式;关闭后统一使用默认模式(默认为标准模式,可自定义)。仅影响新任务。',

+ 0 - 25
packages/client/ui-agent-preset/src/client/slot-contract.ts

@@ -1,25 +0,0 @@
-/**
- * The `settings.agentPreset.detail` slot type — the sections of one preset's
- * detail page under the Agent presets settings section, rendered in
- * registration order once a person opens a preset's configuration from its
- * card. The owner share names the preset; a registrant renders what it knows
- * about that preset (the plugin manager contributes the preset's capabilities).
- *
- * TYPE HOME RATIONALE: the section declares this slot at runtime, and a
- * plugin registering a detail section already depends on this package for the
- * declaration. The type therefore lives with its declarer.
- */
-declare module '@deepseek-ai/dsh-client-ui-slots' {
-  interface SlotMap {
-    /** One section of a preset's detail page (see module JSDoc). */
-    'settings.agentPreset.detail': { kind: 'list'; scope: 'root'; owner: AgentPresetDetailOwnerProps }
-  }
-}
-
-/** Owner share of a detail section: which preset the page shows. */
-export interface AgentPresetDetailOwnerProps {
-  /** The preset's id. */
-  readonly presetId: string
-  /** The preset's display name, resolved through the roster dictionaries. */
-  readonly presetName: string
-}

+ 0 - 5
packages/client/ui-agent-preset/tests/apply.client.spec.ts

@@ -229,11 +229,6 @@ describe('ui-agent-preset apply', () => {
     expect(section.options).toMatchObject({ id: 'agent-presets', order: 20 })
     // The nav label is a locale-following thunk; owners resolve it at read time.
     expect(resolveSlotLabel(section.options.label)).toBe('Agent 预设')
-    // The section declares the detail page's child slot: a contribution to it
-    // registers instead of throwing for an undeclared key.
-    const probe = slots.register({ name: 'settings.agentPreset.detail', id: 'probe' } as never, () => null)
-    expect(slots.entries('settings.agentPreset.detail')).toHaveLength(1)
-    probe()
   })
 
   it('registers into a declaration that arrives after apply', async () => {

+ 1 - 39
packages/client/ui-agent-preset/tests/section.client.spec.tsx

@@ -61,8 +61,6 @@ function renderSection(
     confirmDelete: vi.fn(),
     remove: vi.fn(() => Promise.resolve()),
     makeDefault: vi.fn(() => Promise.resolve()),
-    // The detail page's child slot, rendered by the shell in the real tree.
-    renderSlot: vi.fn(() => <p>detail-slot</p>),
     setPickerVisible: vi.fn(() => Promise.resolve()),
   }
   const props = {
@@ -71,7 +69,7 @@ function renderSection(
     t: (key: keyof typeof en) => en[key],
   } as unknown as AgentPresetSectionProps
   render(<AgentPresetSection {...props} />)
-  return { ...actions, store }
+  return actions
 }
 
 /** Locate a card by the id it prints, not by its display name. */
@@ -571,39 +569,3 @@ describe('a long card description', () => {
     expect(within(rowFor('zh')).getByText(LONG).getAttribute('title')).toBe('')
   })
 })
-
-describe('preset detail', () => {
-  it('opens a preset from its gear, names it, renders the contributed sections, and returns through the breadcrumb', () => {
-    const actions = renderSection()
-
-    fireEvent.click(within(rowFor('standard')).getByRole('button', { name: `${en.configure}: ${en.presetStandardName}` }))
-    expect(screen.getByRole('heading', { name: en.presetStandardName })).toBeTruthy()
-    expect(screen.getByText(en.presetStandardDescription)).toBeTruthy()
-    expect(screen.getByText(en.inUse)).toBeTruthy()
-    expect(screen.getByText('detail-slot')).toBeTruthy()
-    expect(actions.renderSlot).toHaveBeenCalledWith('settings.agentPreset.detail', { presetId: 'standard', presetName: en.presetStandardName })
-    // The roster is off screen while a detail is open.
-    expect(screen.queryByText(en.sectionIntro)).toBeNull()
-
-    fireEvent.click(screen.getByRole('button', { name: en.backToRoster }))
-    expect(screen.getByText(en.sectionIntro)).toBeTruthy()
-    expect(screen.queryByText('detail-slot')).toBeNull()
-  })
-
-  it('shows a custom preset without a description as such, drops back to the roster when it leaves, and withholds the gear on a broken one', () => {
-    const { store } = renderSection({
-      rows: [...READY.rows, { id: 'broken', trust: 'user', isDefault: false, broken: 'bad yaml' }],
-    })
-    expect(within(rowFor('broken')).getByRole('button', { name: `${en.configure}: broken` })).toHaveProperty('disabled', true)
-
-    fireEvent.click(within(rowFor('mine')).getByRole('button', { name: `${en.configure}: mine` }))
-    expect(screen.getByRole('heading', { name: 'mine' })).toBeTruthy()
-    expect(screen.getByText(en.noDescription)).toBeTruthy()
-    expect(screen.getByText(en.userTrust)).toBeTruthy()
-    expect(screen.queryByText(en.inUse)).toBeNull()
-
-    act(() => { store.set({ ...store.getSnapshot(), rows: [READY.rows[0]!] }) })
-    expect(screen.getByText(en.sectionIntro)).toBeTruthy()
-    expect(screen.queryByText('detail-slot')).toBeNull()
-  })
-})

+ 0 - 1
packages/client/ui-permission-presets/tests/permission-presets-row.client.spec.tsx

@@ -39,7 +39,6 @@ const SCHEMA = {
 function view(defaultPreset: string, revision = 0): SettingsNamespaceView {
   return {
     ns: 'permission',
-    registered: true,
     schema: SCHEMA,
     value: { defaultPreset },
     base: { defaultPreset: 'read-only' },

+ 0 - 1
packages/client/ui-permission-presets/tests/settings-store.client.spec.ts

@@ -27,7 +27,6 @@ function resolveDefault(view: SettingsNamespaceView) {
 function view(defaultPreset: string, revision = 0, schema: SettingsNamespaceView['schema'] = SCHEMA): SettingsNamespaceView {
   return {
     ns: 'permission',
-    registered: true,
     schema,
     value: { defaultPreset },
     base: { defaultPreset: 'read-only' },

+ 2 - 2
packages/client/ui-plugin-manager/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/client/ui-plugin-manager/README.md
-README.md: 5c2346183e79049433bedc4cfbc87f96e13da752
-README.zh.md: 50c428417d36a20a16d22b234d57aca059a6cf71
+README.md: 50a29f8c77c9caf22f2e2eba3ecfd72bbe4fee28
+README.zh.md: 10e544ba47844ac3ddfa1120740d0d56963be79a

+ 7 - 7
packages/client/ui-plugin-manager/README.md

@@ -1,5 +1,5 @@
 ---
-description: "Plugin management behind the Web sidebar's Plugins entry for the dsh web client: install packages through pnpm, enable and disable bundles, retry failed ones, and compose rows into the global user layer or one agent preset's."
+description: "Manage installed plugin packages and global rows from the Web sidebar."
 kind: "package-reference"
 ---
 
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-Use the **Plugins** entry in the Web sidebar to see the profile's installed packages in two groups — plugin packs, switched on and off as a whole, and plugins, added to a preset or every session through **Add to…** — open a pack's own page to switch its rows and uninstall it, and install a package by name, path, or Git URL, watching each pnpm run in a terminal. Dependents and every uninstall ask for confirmation. The same store contributes the **Capabilities** section to each preset's detail page; configuring a plugin stays in the Settings **Plugins** section.
+Use the **Plugins** entry in the Web sidebar to manage installed packages. Switch bundle layers on and off, inspect their rows and failures, add declared modules to the global user layer, and install packages while watching pnpm output. Dependency-sensitive changes and uninstalling ask for confirmation. Global configuration remains in Settings.
 
 ## Table of Contents
 
@@ -25,7 +25,7 @@ Use the **Plugins** entry in the Web sidebar to see the profile's installed pack
 <a id="use-this-package"></a>
 ## Use this package
 
-Select **Plugins** in the sidebar. The page reads no Remote during plugin activation — selecting the entry mounts it, and it then reads the packages and the preset compositions through `api-remotes`. Configuring a plugin stays in the Settings **Plugins** section.
+Select **Plugins** in the sidebar. The page reads packages and global composition through `api-remotes` when first opened. Global configuration remains in the Settings **Plugins** section.
 
 ### Installing a package
 
@@ -39,9 +39,9 @@ A pack’s switch changes its enabled layer selection. Live profiles recompose b
 
 A row's switch on the pack's page calls `plugins.setRowDisabled` against the profile's global user layer: off writes `disabled: true` for the row's id into the profile's `cordis.patch.yml`, on deletes that key again. The tree recomposes at once, so the row's host half unmounts or mounts while the rest of the pack keeps running; a browser half the pack's own client bundle mounts for every component stays until the page reloads. The switch appears only where it acts at once — an external pack that is switched on, on a profile that applies patches while it runs; built-in packs, packs that are off, and profiles that apply patches at their next start list their rows read-only. A row the pack itself keeps off, by a `disabled: true` row or a `!!js` gate, is locked with the reason, because the user layer only ever denies. A failing row can be switched off; a row another layer already owns has nothing mounted to switch. Switching a row off first asks the Host what other rows inject of what the row provides, with the switch inert meanwhile, and opens a confirmation naming them only when there are any; a row nothing depends on switches off at once, and switching on never asks. A list longer than ten rows gets a filter over the row ids.
 
-### Adding a plugin to a preset
+### Adding a plugin globally
 
-A package with explicit plugin declarations offers **Add to…** for every declared entry, including a declared main export. The menu marks targets already joined. Addition writes a global or preset user patch; it performs no import precheck. A package with no recognized declaration has no Add action and remains uninstallable. Preset rows are managed in the preset detail page’s **Capabilities** section, where switches write or remove the user’s `disabled: true` override and **Delete** removes a user insert.
+A package with explicit `dsh.plugins` declarations offers **Add globally** for each module. An already-added module has a disabled action. Addition writes the profile's `cordis.patch.yml` without an import precheck. Packages without recognized declarations remain uninstallable and have no Add action.
 
 ### Reading a failure
 
@@ -59,11 +59,11 @@ Package management uses the profile’s dependency records: installed bundles ca
 
 ### Registration
 
-The browser plugin registers the `plugins` panel under one id twice: a `sidebar.panellist` entry with order 0, whose localized label the sidebar renders and whose icon this package supplies, and the `main` keyed entry it opens — the management page, root-scoped and bound to no Session; plus one `settings.agentPreset.detail` contribution with id `plugins` — the Capabilities section of every preset's detail page, over the same store. Registration uses `ctx.slots.inject()`, so it follows late slot declaration, redeclaration, locale changes, and teardown without importing the sidebar or layout owner. Preset names resolve through the shared `presetDisplayText` fold over [`ui-agent-preset`](../ui-agent-preset/README.md)'s dictionaries. Harness modules in a preset resolve their display name and one-liner through the page's own dictionary — `name.<slug>` and `desc.<slug>`, tried by the composition row id first (four subagent rows share one module) and then by the unscoped module name without its `dsh-` prefix — and only for modules under the `@deepseek-ai/` scope; a third-party module reads its installed package's `dsh.title` and `description`, or an addable module's own title.
+The browser plugin registers the `plugins` sidebar entry and its `main` panel through `ctx.slots.inject()`, so both follow late slot declaration, locale changes and teardown. The page is global and belongs to no Session. Shipped module labels come from its dictionary; third-party display text comes from package metadata.
 
 ### The store
 
-`PluginManagerController` holds one snapshot: the read status, the packages, the preset groups, the busy keys, the notice, the install dialog, and the pending confirmation. `load` folds concurrent reads into one in-flight read plus one rerun, so an invalidation landing mid-read is never lost. Every action runs under a busy key — the package name, or `<target>:<rowId>` for a row — turns a refused answer into the notice with the Host's code and reason, and re-reads afterwards whatever happened. The plugin's `apply` subscribes `plugins/changed` and `connection/reset` to reload a page that has rendered once, and `plugins/install-log` to fold chunks into the open install's runs by job, the `add` run and the removals that follow it alike, since the Host runs one mutation at a time.
+`PluginManagerController` owns the package and global-module snapshot, busy keys, notices, install progress and confirmations. It coalesces overlapping reads, refreshes after operations and Host changes, and ignores late results after disposal. Install output is grouped by job id.
 
 ### Confirmation
 

+ 7 - 7
packages/client/ui-plugin-manager/README.zh.md

@@ -1,5 +1,5 @@
 ---
-description: "dsh Web 客户端左侧栏「插件」入口背后的插件管理:经 pnpm 安装包、启停 bundle、重试失败的 bundle,并把行组合进全局用户层或某个 Agent 预设。"
+description: "从 Web 侧栏管理已安装插件包与全局插件行。"
 kind: "package-reference"
 ---
 
@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-使用 Web 左侧栏的**插件**入口可以按两组查看 profile 已安装的包——插件包整体开关、插件经**加入到…**加入某个预设或所有会话——打开插件包自己的页面来开关它的行并卸载它,也可以按包名、路径或 Git 地址安装包,并在一块终端里观看每次 pnpm 运行。有依赖方时与每次卸载都要确认。同一个 store 还把**能力**段贡献到每个预设的详情页;插件的配置仍留在设置的**插件**分区。
+使用 Web 侧栏的**插件**入口管理已安装的包。可以启停组合包层、检查插件行与失败、把声明的模块添加到全局用户层,并在安装时查看 pnpm 输出。影响依赖的修改与卸载会要求确认。全局配置仍在设置中编辑。
 
 ## 目录
 
@@ -25,7 +25,7 @@ kind: "package-reference"
 <a id="use-this-package"></a>
 ## 使用本包
 
-在左侧栏选择**插件**。插件激活期间不会读取 Remote——选择该入口时才挂载页面,页面再通过 `api-remotes` 读取包与预设组合。插件的配置仍在设置的**插件**分区。
+在侧栏选择**插件**。页面首次打开时通过 `api-remotes` 读取包与全局组合。全局配置仍在设置的**插件**分区中编辑。
 
 ### 安装一个包
 
@@ -39,9 +39,9 @@ kind: "package-reference"
 
 包页面上行的开关以 profile 的全局用户层为目标调用 `plugins.setRowDisabled`:关闭往 profile 的 `cordis.patch.yml` 写入该行 id 的 `disabled: true`,打开再把这个键删掉。树随即重组,该行的宿主半区卸下或挂上,插件包其余部分照常运行;插件包自己的客户端 bundle 为每个组件挂载的浏览器半区要到页面重载才消失。开关只在立即生效的地方出现——已打开的外部插件包,且 profile 在运行中就应用补丁;内置插件包、已关闭的插件包,以及下次启动才应用补丁的 profile,其行只读列出。插件包自己关掉的行,无论是 `disabled: true` 的行还是 `!!js` 门,都带原因锁定,因为用户层只能拒绝。异常的行可以关掉;已被别的层占用 id 的行没有挂载任何东西,无从切换。关闭一行前先向宿主询问有哪些行注入它提供的服务,询问期间开关置灰,有则弹出确认框点名它们;没有依赖方的行直接关闭,打开从不询问。超过十行的列表带一个按行 id 筛选的输入框。
 
-### 把插件加入预设
+### 将插件添加到全局
 
-有显式插件声明的包为每个声明入口提供**加入到…**,包括已声明的主入口。菜单标记已加入的目标。添加会写入全局或预设用户 patch,不进行导入预检。未识别到声明的包没有添加操作,但仍可卸载。预设行在预设详情页的**能力**区域管理:开关写入或删除用户的 `disabled: true` 覆盖,**删除**移除用户插入。
+带有显式 `dsh.plugins` 声明的包为各模块提供**加入全局**操作。已加入模块的操作不可点击。添加会写入 profile 的 `cordis.patch.yml`,不预先导入模块检查。没有可识别声明的包仍可卸载,但没有添加操作。
 
 ### 阅读失败
 
@@ -59,11 +59,11 @@ kind: "package-reference"
 
 ### 注册
 
-浏览器插件用同一个 id 把 `plugins` 面板注册两次:一个 order 为 0 的 `sidebar.panellist` 条目,其本地化标签由侧栏渲染、图标由本包提供,以及它打开的 `main` 键控条目——管理页面,根级作用域,不绑定任何 Session;另注册一个 id 为 `plugins` 的 `settings.agentPreset.detail` 贡献——每个预设详情页的能力段,读同一个 store。注册使用 `ctx.slots.inject()`,因此能跟随 slot 的延迟声明、重新声明、本地化变化与 teardown,而无需 import 侧栏或布局的拥有方。预设名经共享的 `presetDisplayText` 纯函数叠在 [`ui-agent-preset`](../ui-agent-preset/README.zh.md) 的字典上解析。预设里的 harness 模块经页面自己的字典解析显示名与一句话——`name.<slug>` 与 `desc.<slug>`,先按组合行 id 查(四个子代理行共用一个模块),再按去掉 `dsh-` 前缀的无 scope 模块名查——且只对 `@deepseek-ai/` scope 下的模块生效;第三方模块读取其安装包的 `dsh.title` 与 `description`,或可添加模块自己的标题。
+浏览器插件通过 `ctx.slots.inject()` 注册 `plugins` 侧栏入口与它的 `main` 面板,使两者跟随 slot 延迟声明、本地化变化与销毁。页面为全局页面,不属于任何 Session。随附模块的标签来自字典,第三方显示文本来自包元信息。
 
 ### store
 
-`PluginManagerController` 持有一份快照:读取状态、包、预设组、忙碌键、提示、安装对话框与待确认项。`load` 把并发读取折叠成一次在途读取加一次重跑,因此读取中途到达的失效不会丢失。每个操作在一个忙碌键下运行——包名,或行的 `<target>:<rowId>`——把被拒的应答转成带宿主代码与原因的提示,并且无论结果如何随后重新读取。插件的 `apply` 订阅 `plugins/changed` 与 `connection/reset` 以重载渲染过的页面,订阅 `plugins/install-log` 按 job 把块折进打开的安装的各次运行——`add` 运行与随后的移除运行一视同仁,因为宿主一次只跑一个变更。
+`PluginManagerController` 拥有包与全局模块快照、忙碌键、提示、安装进度和确认状态。它合并重叠读取,在操作或 Host 变化后刷新,并在销毁后忽略晚到结果。安装输出按 job id 分组。
 
 ### 确认
 

+ 0 - 2
packages/client/ui-plugin-manager/package.json

@@ -47,12 +47,10 @@
   },
   "devDependencies": {
     "@deepseek-ai/cordis": "workspace:^",
-    "@deepseek-ai/dsh-agent-presets": "workspace:^",
     "@deepseek-ai/dsh-api-remotes": "workspace:^",
     "@deepseek-ai/dsh-client-locale": "workspace:^",
     "@deepseek-ai/dsh-client-store": "workspace:^",
     "@deepseek-ai/dsh-client-test-runtime": "workspace:^",
-    "@deepseek-ai/dsh-client-ui-agent-preset": "workspace:^",
     "@deepseek-ai/dsh-client-ui-layout": "workspace:^",
     "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
     "@deepseek-ai/dsh-client-ui-renderer": "workspace:^",

+ 1 - 4
packages/client/ui-plugin-manager/src/client/NoticeLine.tsx

@@ -1,7 +1,4 @@
-/**
- * The outcome line of the last plugin operation, shared by the package list
- * and a preset's section.
- */
+/** One operation notice on the plugin management page. */
 
 import type { ReactNode } from 'react'
 import { Button } from '@deepseek-ai/dsh-client-ui-primitives'

+ 1 - 2
packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css

@@ -1,4 +1,4 @@
-/* Plugin manager page: a centred column with its header, notices, and the package and preset cards. */
+/* Plugin manager page: a centred column with its header, notices, and the package cards. */
 
 .page {
   display: flex;
@@ -250,7 +250,6 @@
   white-space: nowrap;
 }
 
-/* The pill the Agent preset section marks its cards with. */
 .cardEnd {
   position: relative;
   z-index: 1;

+ 34 - 117
packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx

@@ -9,12 +9,11 @@
  * and module names stay in the detail view. A card opens the package's own
  * page: its version and source, its rows — each with its own switch on an
  * external pack composed on a live-reload profile — the built-in rows it
- * changes, the modules it declares addable, and its uninstall. A preset's
- * composition lives on the preset's own detail page (`PresetPluginsSection`).
+ * changes, the modules it declares addable, and its uninstall.
  */
 
 import { useEffect, useId, useState, type ReactNode } from 'react'
-import type { PluginInstallRejection, PluginPackageView, PluginRowTarget } from '@deepseek-ai/dsh-api-remotes/client'
+import type { PluginInstallRejection, PluginPackageView } from '@deepseek-ai/dsh-api-remotes/client'
 import {
   Button, IconChevronDownOutline14, IconCordisPluginOutline14, IconRefreshOutline16,
   Input, Menu, Modal, StateDot, Switch, Tag, TerminalBlock,
@@ -22,7 +21,7 @@ import {
 } from '@deepseek-ai/dsh-client-ui-primitives'
 import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
 import type { PluginManagerLocaleKey } from './locales.ts'
-import { rowKey, type ConfirmState, type InstallState, type PluginManagerFace, type PresetGroup } from './manager-store.ts'
+import { rowKey, type ConfirmState, type InstallState, type PluginManagerFace } from './manager-store.ts'
 import { NoticeLine } from './NoticeLine.tsx'
 import { packageOf, refusalText, rowLabel, shortName, type Translate } from './presentation.ts'
 import css from './PluginManagerPage.module.css'
@@ -37,7 +36,6 @@ type RowView = PluginPackageView['rows'][number]
 type RowPhase = NonNullable<RowView['phase']>
 
 /** The layer a pack's components are switched in: a row override always lands in the profile's global user layer. */
-const GLOBAL: PluginRowTarget = { kind: 'global' }
 
 /** The list's two groups: packs, which switch as a whole, and everything else, which joins a composition per row. */
 const PACKAGE_GROUPS = [
@@ -255,28 +253,14 @@ function OverridesSection({ overrides, t }: { readonly overrides: readonly strin
   )
 }
 
-/** Where one module is composed already: every session, then the presets that carry it. */
-function joinedTargets(
-  entry: PluginPackageView['addable'][number], presets: readonly PresetGroup[], globalModules: readonly string[],
-  t: Translate, presetName: (preset: PresetGroup) => string,
-): string[] {
-  return [
-    ...globalModules.includes(entry.moduleName) ? [t('joinedGlobal')] : [],
-    ...presets.filter(preset => preset.rows.some(row => row.moduleName === entry.moduleName)).map(presetName),
-  ]
-}
-
-/** The modules a package declares addable: each with where it is composed, or why it cannot be, and its **Add to…** menu. */
-function ModulesSection({ pkg, t, busy, presets, globalModules, presetName, onAddRow }: {
+/** Declared modules with their global composition state and add action. */
+function ModulesSection({ pkg, t, busy, globalModules, onAddRow }: {
   readonly pkg: PluginPackageView
   readonly t: Translate
   readonly busy: boolean
-  readonly presets: readonly PresetGroup[]
   readonly globalModules: readonly string[]
-  readonly presetName: (preset: PresetGroup) => string
-  readonly onAddRow: (declaredName: string, target: PluginRowTarget) => void
+  readonly onAddRow: (declaredName: string) => void
 }): ReactNode {
-  const [openMenu, setOpenMenu] = useState<string | null>(null)
   const title = pkg.title ?? shortName(pkg.name)
   return (
     <section className={css.detailSection} data-plugin-modules>
@@ -285,8 +269,7 @@ function ModulesSection({ pkg, t, busy, presets, globalModules, presetName, onAd
       </div>
       <ul className={css.rows}>
         {pkg.addable.map((entry) => {
-          const joined = joinedTargets(entry, presets, globalModules, t, presetName)
-          const open = openMenu === entry.moduleName
+          const added = globalModules.includes(entry.moduleName)
           return (
             <li key={entry.moduleName} className={css.row} data-plugin-module={entry.moduleName}>
               <div className={css.rowLine}>
@@ -294,34 +277,11 @@ function ModulesSection({ pkg, t, busy, presets, globalModules, presetName, onAd
                 <div className={css.rowMain}>
                   <span className={css.rowId}>{entry.title ?? (entry.declaredName === '.' ? title : entry.declaredName)}</span>
                   <span className={css.rowModule}>{entry.moduleName}</span>
-                  <span className={css.rowNote}>
-                    {joined.length > 0 ? t('moduleJoined', { targets: joined.join(t('joinedSeparator')) }) : t('moduleNotJoined')}
-                  </span>
+                  <span className={css.rowNote}>{t(added ? 'joinedGlobal' : 'moduleNotJoined')}</span>
                 </div>
-                <Menu
-                  open={open}
-                  onClose={() => { setOpenMenu(null) }}
-                  items={addTargets(entry, presets, globalModules, t, presetName)}
-                  onSelect={(id) => {
-                    setOpenMenu(null)
-                    const choice = JSON.parse(id) as AddChoice
-                    onAddRow(choice.declaredName, choice.target)
-                  }}
-                  align="end"
-                  portal
-                  anchor={(
-                    <Button
-                      variant="outline"
-                      size="sm"
-                      aria-haspopup="menu"
-                      aria-expanded={open}
-                      disabled={busy}
-                      onClick={() => { setOpenMenu(open ? null : entry.moduleName) }}
-                    >
-                      {t('addTo')}
-                    </Button>
-                  )}
-                />
+                <Button variant="outline" size="sm" disabled={busy || added} onClick={() => { onAddRow(entry.declaredName) }}>
+                  {t(added ? 'addToGlobalAdded' : 'addToGlobal')}
+                </Button>
               </div>
             </li>
           )
@@ -331,32 +291,6 @@ function ModulesSection({ pkg, t, busy, presets, globalModules, presetName, onAd
   )
 }
 
-/** One **Add to…** choice, carried through the menu as its item id. */
-interface AddChoice {
-  readonly declaredName: string
-  readonly target: PluginRowTarget
-}
-
-/** The **Add to…** menu entries of one addable module; each id carries the choice it makes. */
-function addTargets(
-  entry: PluginPackageView['addable'][number], presets: readonly PresetGroup[], globalModules: readonly string[],
-  t: Translate, presetName: (preset: PresetGroup) => string,
-): MenuItem[] {
-  const choice = (target: PluginRowTarget): string => JSON.stringify({ declaredName: entry.declaredName, target } satisfies AddChoice)
-  const inGlobal = globalModules.includes(entry.moduleName)
-  return [
-    { id: choice({ kind: 'global' }), label: t(inGlobal ? 'addToGlobalAdded' : 'addToGlobal'), disabled: inGlobal },
-    ...presets.map((preset) => {
-      const added = preset.rows.some(row => row.source === 'user' && row.moduleName === entry.moduleName)
-      return {
-        id: choice({ kind: 'preset', preset: preset.id }),
-        label: t(added ? 'addToPresetAdded' : 'addToPreset', { name: presetName(preset) }),
-        disabled: added || preset.broken !== undefined,
-      }
-    }),
-  ]
-}
-
 /** A bundle's enable switch on its card and its page: locked for a built-in bundle, off and locked for one the profile cannot enable. */
 function EnableSwitch({ pkg, title, t, busy, onSetEnabled }: {
   readonly pkg: PluginPackageView
@@ -379,16 +313,14 @@ function EnableSwitch({ pkg, title, t, busy, onSetEnabled }: {
 }
 
 /** One installed package as a card that opens its page: its name, its one-liner, its tags, and its switch or its **Add to…** menu. */
-function PackageCard({ pkg, t, busy, presets, globalModules, presetName, onOpen, onSetEnabled, onAddRow }: {
+function PackageCard({ pkg, t, busy, globalModules, onOpen, onSetEnabled, onAddRow }: {
   readonly pkg: PluginPackageView
   readonly t: Translate
   readonly busy: boolean
-  readonly presets: readonly PresetGroup[]
   readonly globalModules: readonly string[]
-  readonly presetName: (preset: PresetGroup) => string
   readonly onOpen: () => void
   readonly onSetEnabled: (enabled: boolean) => void
-  readonly onAddRow: (declaredName: string, target: PluginRowTarget) => void
+  readonly onAddRow: (declaredName: string) => void
 }): ReactNode {
   const [addMenu, setAddMenu] = useState(false)
   const title = pkg.title ?? shortName(pkg.name)
@@ -396,13 +328,11 @@ function PackageCard({ pkg, t, busy, presets, globalModules, presetName, onOpen,
   const status = cardStatus(pkg)
   const addable = pkg.addable
   const [single] = addable
-  const menuItems: MenuItem[] = single !== undefined && addable.length === 1
-    ? addTargets(single, presets, globalModules, t, presetName)
-    : addable.map(entry => ({
-      id: entry.moduleName,
-      label: entry.title ?? entry.declaredName,
-      submenu: addTargets(entry, presets, globalModules, t, presetName),
-    }))
+  const menuItems: MenuItem[] = addable.map(entry => ({
+    id: entry.declaredName,
+    label: entry.title ?? entry.declaredName,
+    disabled: globalModules.includes(entry.moduleName),
+  }))
   return (
     <li className={`${css.card} ${css.cardLink}`} data-plugin-package={pkg.name} data-plugin-status={pkg.status}>
       <div className={css.cardHead}>
@@ -418,16 +348,18 @@ function PackageCard({ pkg, t, busy, presets, globalModules, presetName, onOpen,
           <EnableSwitch pkg={pkg} title={title} t={t} busy={busy} onSetEnabled={onSetEnabled} />
           {menuItems.length === 0
             ? null
-            : (
+            : single !== undefined && addable.length === 1 ? (
+              <Button variant="outline" size="sm" disabled={busy || globalModules.includes(single.moduleName)} onClick={() => { onAddRow(single.declaredName) }}>
+                {t(globalModules.includes(single.moduleName) ? 'addToGlobalAdded' : 'addToGlobal')}
+              </Button>
+            ) : (
               <Menu
                 open={addMenu}
                 onClose={() => { setAddMenu(false) }}
                 items={menuItems}
                 onSelect={(id) => {
                   setAddMenu(false)
-                  // Only a leaf carries a choice; a submenu parent's id is its module name.
-                  const choice = JSON.parse(id) as AddChoice
-                  onAddRow(choice.declaredName, choice.target)
+                  onAddRow(id)
                 }}
                 align="end"
                 portal
@@ -440,7 +372,7 @@ function PackageCard({ pkg, t, busy, presets, globalModules, presetName, onOpen,
                     disabled={busy}
                     onClick={() => { setAddMenu(current => !current) }}
                   >
-                    {t('addTo')}
+                    {t('addToGlobal')}
                   </Button>
                 )}
               />
@@ -458,7 +390,7 @@ function PackageCard({ pkg, t, busy, presets, globalModules, presetName, onOpen,
  * declares addable with where each is composed; and retry and uninstall.
  */
 function PackageDetail({
-  pkg, t, busy, rowBusy, presets, globalModules, presetName,
+  pkg, t, busy, rowBusy, globalModules,
   onBack, onSetEnabled, onRetry, onUninstall, onAddRow, onSetRowDisabled,
 }: {
   readonly pkg: PluginPackageView
@@ -466,14 +398,12 @@ function PackageDetail({
   readonly busy: boolean
   /** Whether a row has a write in flight. */
   readonly rowBusy: (rowId: string) => boolean
-  readonly presets: readonly PresetGroup[]
   readonly globalModules: readonly string[]
-  readonly presetName: (preset: PresetGroup) => string
   readonly onBack: () => void
   readonly onSetEnabled: (enabled: boolean) => void
   readonly onRetry: () => void
   readonly onUninstall: () => void
-  readonly onAddRow: (declaredName: string, target: PluginRowTarget) => void
+  readonly onAddRow: (declaredName: string) => void
   readonly onSetRowDisabled: (row: RowView, disabled: boolean) => void
 }): ReactNode {
   const title = pkg.title ?? shortName(pkg.name)
@@ -563,9 +493,7 @@ function PackageDetail({
               pkg={pkg}
               t={t}
               busy={busy}
-              presets={presets}
               globalModules={globalModules}
-              presetName={presetName}
               onAddRow={onAddRow}
             />
           )}
@@ -713,12 +641,10 @@ const CONFIRM_KEYS = {
 } as const satisfies Record<ConfirmState['action'], Record<'titleKey' | 'descriptionKey' | 'actionKey', PluginManagerLocaleKey>>
 
 /** The confirmation a destructive action waits on, naming what still uses the package or the row. */
-function ConfirmDialog({ confirm, t, packages, presets, presetName, onConfirm, onCancel }: {
+function ConfirmDialog({ confirm, t, packages, onConfirm, onCancel }: {
   readonly confirm: ConfirmState
   readonly t: Translate
   readonly packages: readonly PluginPackageView[]
-  readonly presets: readonly PresetGroup[]
-  readonly presetName: (preset: PresetGroup) => string
   readonly onConfirm: () => void
   readonly onCancel: () => void
 }): ReactNode {
@@ -733,10 +659,7 @@ function ConfirmDialog({ confirm, t, packages, presets, presetName, onConfirm, o
       ...dependents.references.map((reference) => {
         const owner = packageOf(reference.moduleName, packages)
         const row = owner === undefined ? reference.rowId : owner.title ?? shortName(owner.name)
-        if (reference.target.kind === 'global') return t('dependentReferenceGlobal', { row })
-        const target = reference.target.preset
-        const preset = presets.find(candidate => candidate.id === target)
-        return t('dependentReferencePreset', { name: preset === undefined ? target : presetName(preset), row })
+        return t('dependentReferenceGlobal', { row })
       }),
     ]
   return (
@@ -770,7 +693,7 @@ function ConfirmDialog({ confirm, t, packages, presets, presetName, onConfirm, o
 
 /** Render the plugin manager: the installed packages, the install dialog, and the confirmation. */
 export function PluginManagerPage(props: PluginManagerPageProps): ReactNode {
-  const { t, presetName, ensure } = props
+  const { t, ensure } = props
   const state = props.usePluginManager(snapshot => snapshot)
   // The package whose page is open; one that leaves the list (uninstalled) drops back to the cards.
   const [openPackage, setOpenPackage] = useState<string | null>(null)
@@ -818,19 +741,17 @@ export function PluginManagerPage(props: PluginManagerPageProps): ReactNode {
             pkg={openPkg}
             t={t}
             busy={state.busy.includes(openPkg.name)}
-            rowBusy={rowId => state.busy.includes(rowKey(GLOBAL, rowId))}
-            presets={state.presets}
+            rowBusy={rowId => state.busy.includes(rowKey(rowId))}
             globalModules={state.globalModules}
-            presetName={presetName}
             onBack={() => { setOpenPackage(null) }}
             onSetEnabled={(enabled) => { props.setEnabled(openPkg.name, enabled) }}
             onRetry={() => { props.retry(openPkg.name) }}
             onUninstall={() => { props.uninstall(openPkg.name) }}
-            onAddRow={(declaredName, target) => { props.addRow(openPkg.name, declaredName, target) }}
+            onAddRow={(declaredName) => { props.addRow(openPkg.name, declaredName) }}
             onSetRowDisabled={(row, disabled) => {
               // Off asks first when other rows inject what the row provides; on has nothing to ask.
               if (disabled) props.disableRow(openPkg.name, row.entryId, row.rowId)
-              else props.setRowDisabled(GLOBAL, row.rowId, false)
+              else props.setRowDisabled(row.rowId, false)
             }}
           />
         )
@@ -855,12 +776,10 @@ export function PluginManagerPage(props: PluginManagerPageProps): ReactNode {
                         pkg={pkg}
                         t={t}
                         busy={state.busy.includes(pkg.name)}
-                        presets={state.presets}
                         globalModules={state.globalModules}
-                        presetName={presetName}
                         onOpen={() => { setOpenPackage(pkg.name) }}
                         onSetEnabled={(enabled) => { props.setEnabled(pkg.name, enabled) }}
-                        onAddRow={(declaredName, target) => { props.addRow(pkg.name, declaredName, target) }}
+                        onAddRow={(declaredName) => { props.addRow(pkg.name, declaredName) }}
                       />
                     ))}
                   </ul>
@@ -883,8 +802,6 @@ export function PluginManagerPage(props: PluginManagerPageProps): ReactNode {
             confirm={state.confirm}
             t={t}
             packages={state.packages}
-            presets={state.presets}
-            presetName={presetName}
             onConfirm={props.confirm}
             onCancel={props.cancelConfirm}
           />

+ 0 - 181
packages/client/ui-plugin-manager/src/client/PresetPluginsSection.tsx

@@ -1,181 +0,0 @@
-/**
- * The capabilities section of one agent preset's detail page: the preset's
- * composition as cards with a switch each, a delete for rows the person
- * added, and an **Add** menu over the installed plugins the preset does not
- * carry yet. Contributed to the roster's `settings.agentPreset.detail` slot;
- * it reads the same store as the Manage plugins tab, so a change made on
- * either surface shows on the other at once.
- */
-
-import { useEffect, useState, type ReactNode } from 'react'
-import type { PluginPackageView } from '@deepseek-ai/dsh-api-remotes/client'
-import { Button, IconTrashOutline16, Menu, Switch, Tag, type MenuItem } from '@deepseek-ai/dsh-client-ui-primitives'
-import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
-// Type-only: the detail slot's declaration, owned by the roster.
-import type {} from '@deepseek-ai/dsh-client-ui-agent-preset/client'
-import type { PluginManagerFace, PresetGroup, PresetRow } from './manager-store.ts'
-import { rowKey } from './manager-store.ts'
-import { NoticeLine } from './NoticeLine.tsx'
-import { presetRowCopy, rowIdOf, shortName, type Translate } from './presentation.ts'
-import css from './PluginManagerPage.module.css'
-
-/** Full component props assembled by the slot renderer. */
-export type PresetPluginsSectionProps =
-  PropsRuntime<'settings.agentPreset.detail'>
-  & PropsLocale<'pluginManager'>
-  & InjectFace<PluginManagerFace>
-
-/** One module the preset could still add, carried through the menu as its item id. */
-interface AddChoice {
-  readonly packageName: string
-  readonly declaredName: string
-}
-
-/** The installed modules the preset does not carry yet, as menu items. */
-function addChoices(packages: readonly PluginPackageView[], preset: PresetGroup, t: Translate): MenuItem[] {
-  const carried = new Set(preset.rows.map(row => row.moduleName))
-  const items: MenuItem[] = []
-  for (const pkg of packages) {
-    const title = pkg.title ?? shortName(pkg.name)
-    for (const entry of pkg.addable) {
-      if (carried.has(entry.moduleName)) continue
-      items.push({
-        id: JSON.stringify({ packageName: pkg.name, declaredName: entry.declaredName } satisfies AddChoice),
-        label: entry.title ?? (entry.declaredName === '.' ? title : `${title} · ${entry.declaredName}`),
-      })
-    }
-  }
-  return items.length === 0 ? [{ id: 'none', label: t('capabilitiesAddEmpty'), disabled: true }] : items
-}
-
-/** One row of the preset's composition, as a card with its switch. */
-function PresetCard({ preset, row, packages, t, busy, onSetDisabled, onRemove }: {
-  readonly preset: PresetGroup
-  readonly row: PresetRow
-  readonly packages: readonly PluginPackageView[]
-  readonly t: Translate
-  readonly busy: boolean
-  readonly onSetDisabled: (rowId: string, disabled: boolean) => void
-  readonly onRemove: (rowId: string) => void
-}): ReactNode {
-  const id = rowIdOf(row.entryId)
-  const copy = presetRowCopy(row, packages, t)
-  const lockedOff = row.enabled === false && row.disabledBy === 'composition'
-  return (
-    <li className={css.card} data-preset-row={row.entryId ?? undefined} data-plugin-source={row.source}>
-      <div className={css.cardHead}>
-        <div className={css.cardMain}>
-          <div className={css.titleRow}>
-            <span className={css.cardTitle}>{copy.title}</span>
-            {copy.local ? <Tag tone="info">{t('localTag')}</Tag> : null}
-            {row.fiberPhase === 'failed' ? <Tag tone="danger">{t('rowStateFailed')}</Tag> : null}
-          </div>
-          {copy.description === undefined ? null : <span className={css.cardDesc}>{copy.description}</span>}
-        </div>
-        <div className={css.cardEnd}>
-          {id === null
-            ? <span className={css.rowNote} title={t('rowNoId')}>{t('rowNoId')}</span>
-            : (
-              <>
-                {row.source === 'user'
-                  ? (
-                    <button
-                      type="button"
-                      className={css.deleteButton}
-                      aria-label={t('rowRemoveLabel', { name: copy.title })}
-                      title={t('rowRemove')}
-                      disabled={busy}
-                      onClick={() => { onRemove(id) }}
-                    >
-                      <IconTrashOutline16 />
-                    </button>
-                  )
-                  : null}
-                <Switch
-                  checked={row.enabled !== false}
-                  label={t('rowToggle', { name: copy.title })}
-                  {...lockedOff ? { title: t('rowLockedByComposition') } : {}}
-                  disabled={busy || preset.broken !== undefined || lockedOff}
-                  onChange={() => { onSetDisabled(id, row.enabled !== false) }}
-                />
-              </>
-            )}
-        </div>
-      </div>
-    </li>
-  )
-}
-
-/** Render one preset's capabilities: its rows, the add menu, and the last action's outcome. */
-export function PresetPluginsSection(props: PresetPluginsSectionProps): ReactNode {
-  const { t, presetId, presetName, ensure } = props
-  const state = props.usePluginManager(snapshot => snapshot)
-  const [addMenu, setAddMenu] = useState(false)
-  useEffect(() => { ensure() }, [ensure])
-
-  const target = { kind: 'preset', preset: presetId } as const
-  const preset = state.presets.find(candidate => candidate.id === presetId)
-  const loaded = state.status === 'ready' || state.status === 'error'
-  return (
-    <section className={css.group} data-plugin-scope="preset" data-preset-id={presetId}>
-      <div className={css.groupTitleRow}>
-        <h3 className={css.groupTitle}>{t('capabilitiesTitle')}</h3>
-        {preset === undefined || preset.broken !== undefined
-          ? null
-          : (
-            <Menu
-              open={addMenu}
-              onClose={() => { setAddMenu(false) }}
-              items={addChoices(state.packages, preset, t)}
-              onSelect={(id) => {
-                setAddMenu(false)
-                const choice = JSON.parse(id) as AddChoice
-                props.addRow(choice.packageName, choice.declaredName, target)
-              }}
-              align="end"
-              portal
-              anchor={(
-                <Button
-                  variant="outline"
-                  size="sm"
-                  aria-haspopup="menu"
-                  aria-expanded={addMenu}
-                  aria-label={t('capabilitiesAddLabel', { name: presetName })}
-                  disabled={state.busy.length > 0}
-                  onClick={() => { setAddMenu(current => !current) }}
-                >
-                  {t('capabilitiesAdd')}
-                </Button>
-              )}
-            />
-          )}
-      </div>
-      {state.status === 'loading' ? <p className={css.status}>{t('loading')}</p> : null}
-      {state.status === 'unavailable' ? <p className={css.status} role="status">{t('unavailable')}</p> : null}
-      {state.status === 'error' ? <p className={css.reason} role="alert">{t('error')}</p> : null}
-      <NoticeLine notice={state.notice} t={t} onDismiss={props.dismissNotice} />
-      {!loaded || preset !== undefined ? null : <p className={css.empty}>{t('presetMissing')}</p>}
-      {preset?.broken === undefined ? null : <p className={css.reason} role="alert">{preset.broken}</p>}
-      {preset === undefined || preset.broken !== undefined
-        ? null
-        : preset.rows.length === 0
-          ? <p className={css.empty}>{t('presetRowsEmpty')}</p>
-          : (
-            <ul className={css.cards}>
-              {preset.rows.map((row, index) => (
-                <PresetCard
-                  key={`${row.entryId ?? row.moduleName}:${String(index)}`}
-                  preset={preset}
-                  row={row}
-                  packages={state.packages}
-                  t={t}
-                  busy={row.entryId !== null && state.busy.includes(rowKey(target, rowIdOf(row.entryId) as string))}
-                  onSetDisabled={(rowId, disabled) => { props.setRowDisabled(target, rowId, disabled) }}
-                  onRemove={(rowId) => { props.removeRow(target, rowId) }}
-                />
-              ))}
-            </ul>
-          )}
-    </section>
-  )
-}

+ 5 - 23
packages/client/ui-plugin-manager/src/client/index.ts

@@ -1,9 +1,8 @@
 /**
  * Plugin manager, browser half: the **Plugins** entry of the sidebar and the
- * management page it opens in the main column, and the capabilities section
- * of every agent preset's detail page. The page installs, enables, disables,
+ * management page it opens in the main column. The page installs, enables, disables,
  * retries, and uninstalls the packages of the Host's profile through the
- * `plugins` Remote; the section composes rows into one preset's user layer.
+ * `plugins` Remote and edits global rows in the profile's user layer.
  * Configuring a plugin stays in Settings.
  */
 
@@ -20,22 +19,14 @@ import type {} from '@deepseek-ai/dsh-api-remotes/client'
 // Type-only: the forwarded events' own declaration (`$on`'s key face resolves
 // through the owning package's client-safe types subpath).
 import type {} from '@deepseek-ai/dsh-host-plugin-manager/types'
-// Type-only: the 'settings.agentPreset' LocaleNamespaceMap merge the
-// shipped-preset name resolution reads, and the `settings.agentPreset.detail`
-// slot the capabilities section registers into.
-import type {} from '@deepseek-ai/dsh-client-ui-agent-preset/client'
-// Inline-safe shared fold: shipped ids map to dictionary keys in one home.
-import { presetDisplayText } from '@deepseek-ai/dsh-agent-presets/display'
 import { PluginManagerPage } from './PluginManagerPage.tsx'
 import { PluginsPanelIcon } from './PluginsPanelIcon.tsx'
-import { PresetPluginsSection } from './PresetPluginsSection.tsx'
 import { PluginManagerController } from './manager-store.ts'
 import { en, zh, type PluginManagerLocaleKey } from './locales.ts'
 
 export type { PluginManagerPageProps } from './PluginManagerPage.tsx'
-export type { PresetPluginsSectionProps } from './PresetPluginsSection.tsx'
 export type {
-  ConfirmState, InstallState, ManagerNotice, PluginManagerFace, PluginManagerState, PresetGroup, PresetRow,
+  ConfirmState, InstallState, ManagerNotice, PluginManagerFace, PluginManagerState,
 } from './manager-store.ts'
 export type { PluginManagerLocaleKey } from './locales.ts'
 
@@ -57,15 +48,13 @@ export const inject = ['slots', 'locale', 'remote', 'remote.plugins', 'remote.pl
 
 /**
  * Contribute the Plugins entry to the sidebar with the management page it
- * opens, and the capabilities section to every preset's detail page, and
- * keep both current on the Host's change events.
+ * opens, and keep it current on the Host's change events.
  * @param ctx - the browser plugin context.
  */
 export function apply(ctx: ClientContext): void {
   ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-plugin-manager: dictionaries')
   const t = ctx.locale.bind(NS)
-  const agentPresetCopy = ctx.locale.bind('settings.agentPreset')
-  const controller = new PluginManagerController(ctx, preset => presetDisplayText(preset, agentPresetCopy).name)
+  const controller = new PluginManagerController(ctx)
   ctx.effect(() => () => { controller.dispose() }, 'ui-plugin-manager: controller')
   // The Host says when what is installed, enabled, or composed changed — from
   // this page, the CLI, or another browser — and streams install output.
@@ -99,11 +88,4 @@ export function apply(ctx: ClientContext): void {
     locale: NS,
   }, PluginsPanelIcon))
 
-  ctx.slots.inject('settings.agentPreset.detail', () => ctx.slots.register({
-    name: 'settings.agentPreset.detail',
-    id: 'plugins',
-    order: 0,
-    locale: NS,
-    inject: () => controller.inject(),
-  }, PresetPluginsSection))
 }

+ 7 - 115
packages/client/ui-plugin-manager/src/client/locales.ts

@@ -1,10 +1,4 @@
-/**
- * Copy dictionaries for the plugin manager Settings tab, including the
- * display names and one-line descriptions of the harness's own agent-plane
- * modules (`name.<slug>` / `desc.<slug>`, keyed by the composition row id or
- * the unscoped module name without its `dsh-` prefix). A third-party package
- * names itself through its manifest; these keys cover the shipped presets.
- */
+/** Plugin management copy and display names of shipped global rows. */
 
 /** Simplified Chinese dictionary and key source of truth. */
 export const zh = {
@@ -24,14 +18,7 @@ export const zh = {
   bundlesTitle: '插件包',
   pluginsTitle: '插件',
   countUnit: '个',
-  capabilitiesTitle: '能力',
-  capabilitiesAdd: '添加',
-  capabilitiesAddLabel: '给 {name} 添加能力',
-  capabilitiesAddEmpty: '没有可添加的插件',
-  presetRowsEmpty: '这个预设还没有任何能力。',
-  presetMissing: '这个预设已不存在。',
   builtinTag: '内置',
-  localTag: '本地',
   statusRestart: '需重启',
   statusWaiting: '等待依赖',
   statusProblem: '异常',
@@ -76,19 +63,12 @@ export const zh = {
   moduleNotJoined: '尚未加入',
   joinedGlobal: '所有会话',
   joinedSeparator: '、',
-  addToGlobal: '所有会话',
-  addToGlobalAdded: '所有会话(已加入)',
-  addToPreset: '预设:{name}',
-  addToPresetAdded: '预设:{name}(已加入)',
+  addToGlobal: '加入全局',
+  addToGlobalAdded: '已加入全局',
   retryPackage: '重试',
   uninstall: '卸载',
   uninstallLabel: '卸载 {name}',
   rowStateFailed: '异常',
-  rowToggle: '启用 {name}',
-  rowLockedByComposition: '预设关闭了这个插件,不能在这里打开。',
-  rowRemove: '删除',
-  rowRemoveLabel: '从这个预设删除 {name}',
-  rowNoId: '这一行没有声明 id,无法在这里启停。',
   installTitle: '添加插件',
   installDescription: '输入插件的包名、本地路径或 Git 地址。',
   installSpecLabel: '包名或地址',
@@ -115,7 +95,7 @@ export const zh = {
   terminalNoExitCode: '未正常退出',
   installDoneEnabled: '已安装并启用 {name}。',
   installDoneBundle: '已安装 {name},打开它的开关即可启用。',
-  installDonePlugin: '已安装 {name},可以加入到某个预设。',
+  installDonePlugin: '已安装 {name},可以添加到全局。',
   installDoneOther: '已安装 {name}。',
   installDoneNothing: '安装完成,没有新增依赖。',
   installRemovedInvalid: '{name} 的插件包声明无效,已自动移除:{reason}',
@@ -134,7 +114,6 @@ export const zh = {
   confirmDependents: '这些地方还在用它:',
   dependentService: '{rows} 依赖它提供的功能',
   dependentReferenceGlobal: '所有会话里加入的插件 {row}',
-  dependentReferencePreset: '预设「{name}」里加入的插件 {row}',
   confirmUninstall: '卸载',
   confirmDisable: '停用',
   confirmDisableRowTitle: '关闭「{name}」?',
@@ -146,77 +125,41 @@ export const zh = {
   rowConflict: '已经有同名的插件行:{row}。',
   notInstalled: '{name} 没有安装在当前 profile。',
   'name.persona': '人设',
-  'desc.persona': '助手的身份与语气',
   'name.agent-instructions': '项目指令',
-  'desc.agent-instructions': '读取 AGENTS.md 等项目说明',
   'name.tool-bash': '终端',
-  'desc.tool-bash': '运行 shell 命令',
   'name.tool-pwsh': 'PowerShell 终端',
-  'desc.tool-pwsh': '在 Windows 上运行 PowerShell',
   'name.tool-fs': '文件读写',
-  'desc.tool-fs': '读取、创建和修改文件',
   'name.tool-fs-search': '文件搜索',
-  'desc.tool-fs-search': '按名称和内容查找文件',
   'name.tool-jobs': '后台任务',
-  'desc.tool-jobs': '启动和查看长时间运行的命令',
   'name.skill-filesystem': '技能',
-  'desc.skill-filesystem': '从本地目录加载技能',
   'name.tool-skill': '技能调用',
-  'desc.tool-skill': '让助手调用技能',
   'name.command-goal': '目标命令',
-  'desc.command-goal': '/goal 命令',
   'name.tool-goal': '目标',
-  'desc.tool-goal': '记录并跟踪当前目标',
   'name.plan-mode': '计划模式',
-  'desc.plan-mode': '先规划再动手',
   'name.compaction-basic': '上下文压缩',
-  'desc.compaction-basic': '对话过长时自动整理',
   'name.command-compact': '压缩命令',
-  'desc.command-compact': '/compact 命令',
   'name.tool-result-pruner': '工具结果裁剪',
-  'desc.tool-result-pruner': '丢弃过时的工具输出',
   'name.tool-subagent-control': '子代理控制',
-  'desc.tool-subagent-control': '查看和管理子代理',
   'name.tool-subagent-list-agents': '子代理列表',
-  'desc.tool-subagent-list-agents': '列出正在运行的子代理',
   'name.tool-subagent': '子代理',
-  'desc.tool-subagent': '把任务分派给并行的助手',
   'name.tool-subagent-fork': '分支子代理',
-  'desc.tool-subagent-fork': '复制当前上下文继续任务',
   'name.tool-subagent-codex': 'Codex 子代理',
-  'desc.tool-subagent-codex': '把任务交给 Codex 执行',
   'name.tool-subagent-claude-code': 'Claude Code 子代理',
-  'desc.tool-subagent-claude-code': '把任务交给 Claude Code 执行',
   'name.workflow-ptc': '工作流引擎',
-  'desc.workflow-ptc': '运行多步骤工作流',
   'name.tool-workflow': '工作流',
-  'desc.tool-workflow': '编排多步骤任务',
   'name.tool-ralph': 'Ralph 循环',
-  'desc.tool-ralph': '反复执行直到完成',
   'name.tool-ask-user': '向用户提问',
-  'desc.tool-ask-user': '需要时向你确认',
   'name.tool-todo': '待办',
-  'desc.tool-todo': '维护任务清单',
   'name.tool-web': '网页',
-  'desc.tool-web': '搜索和抓取网页',
   'name.tool-cordis': '动态插件',
-  'desc.tool-cordis': '让模型定义并运行插件',
   'name.tool-presentation': '工具展示',
-  'desc.tool-presentation': '决定工具结果如何展示',
   'name.pty': '终端会话',
-  'desc.pty': '保持一个持久的终端',
   'name.terminal-bash': 'Bash 终端',
-  'desc.terminal-bash': '持久的 bash 会话',
   'name.persistent-bash': '持久终端工具',
-  'desc.persistent-bash': '在同一个终端里连续执行命令',
   'name.terminal-pwsh': 'PowerShell 会话',
-  'desc.terminal-pwsh': '持久的 PowerShell 会话',
   'name.persistent-pwsh': '持久 PowerShell 工具',
-  'desc.persistent-pwsh': '在同一个 PowerShell 里连续执行命令',
   'name.fs-local': '本地文件系统',
-  'desc.fs-local': '访问本机文件',
   'name.str-replace-editor': '文本编辑器',
-  'desc.str-replace-editor': '按片段替换来修改文件',
 } satisfies Record<string, string>
 
 /** Plugin manager locale key union. */
@@ -240,14 +183,7 @@ export const en = {
   bundlesTitle: 'Plugin packs',
   pluginsTitle: 'Plugins',
   countUnit: 'packages',
-  capabilitiesTitle: 'Capabilities',
-  capabilitiesAdd: 'Add',
-  capabilitiesAddLabel: 'Add a capability to {name}',
-  capabilitiesAddEmpty: 'No plugin left to add',
-  presetRowsEmpty: 'This preset has no capabilities yet.',
-  presetMissing: 'This preset no longer exists.',
   builtinTag: 'Built-in',
-  localTag: 'Local',
   statusRestart: 'Restart needed',
   statusWaiting: 'Waiting for a dependency',
   statusProblem: 'Problem',
@@ -292,19 +228,12 @@ export const en = {
   moduleNotJoined: 'Not added yet',
   joinedGlobal: 'Every session',
   joinedSeparator: ', ',
-  addToGlobal: 'All sessions',
-  addToGlobalAdded: 'All sessions (added)',
-  addToPreset: 'Preset: {name}',
-  addToPresetAdded: 'Preset: {name} (added)',
+  addToGlobal: 'Add globally',
+  addToGlobalAdded: 'Added globally',
   retryPackage: 'Retry',
   uninstall: 'Uninstall',
   uninstallLabel: 'Uninstall {name}',
   rowStateFailed: 'Problem',
-  rowToggle: 'Enable {name}',
-  rowLockedByComposition: 'The preset switched this plugin off; it cannot be switched on here.',
-  rowRemove: 'Delete',
-  rowRemoveLabel: 'Delete {name} from this preset',
-  rowNoId: 'This row declares no id, so it cannot be switched here.',
   installTitle: 'Add plugin',
   installDescription: 'Enter the plugin\'s package name, a local path, or a Git URL.',
   installSpecLabel: 'Package or address',
@@ -331,7 +260,7 @@ export const en = {
   terminalNoExitCode: 'no exit code',
   installDoneEnabled: 'Installed and enabled {name}.',
   installDoneBundle: 'Installed {name}; turn its switch on to enable it.',
-  installDonePlugin: 'Installed {name}; it can join a preset.',
+  installDonePlugin: 'Installed {name}; it can be added globally.',
   installDoneOther: 'Installed {name}.',
   installDoneNothing: 'Install finished with no new dependency.',
   installRemovedInvalid: '{name} has invalid plugin pack declarations and was removed: {reason}',
@@ -350,7 +279,6 @@ export const en = {
   confirmDependents: 'These still use it:',
   dependentService: '{rows} depend on what it provides',
   dependentReferenceGlobal: 'the plugin {row} added to all sessions',
-  dependentReferencePreset: 'the plugin {row} added to preset "{name}"',
   confirmUninstall: 'Uninstall',
   confirmDisable: 'Switch off',
   confirmDisableRowTitle: 'Switch off "{name}"?',
@@ -362,75 +290,39 @@ export const en = {
   rowConflict: 'A plugin row with this name already exists: {row}.',
   notInstalled: '{name} is not installed in the current profile.',
   'name.persona': 'Persona',
-  'desc.persona': 'The assistant\'s identity and tone',
   'name.agent-instructions': 'Project instructions',
-  'desc.agent-instructions': 'Reads AGENTS.md and similar project notes',
   'name.tool-bash': 'Terminal',
-  'desc.tool-bash': 'Runs shell commands',
   'name.tool-pwsh': 'PowerShell terminal',
-  'desc.tool-pwsh': 'Runs PowerShell on Windows',
   'name.tool-fs': 'Files',
-  'desc.tool-fs': 'Reads, creates, and edits files',
   'name.tool-fs-search': 'File search',
-  'desc.tool-fs-search': 'Finds files by name and content',
   'name.tool-jobs': 'Background jobs',
-  'desc.tool-jobs': 'Starts and follows long-running commands',
   'name.skill-filesystem': 'Skills',
-  'desc.skill-filesystem': 'Loads skills from local folders',
   'name.tool-skill': 'Skill invocation',
-  'desc.tool-skill': 'Lets the assistant invoke skills',
   'name.command-goal': 'Goal command',
-  'desc.command-goal': 'The /goal command',
   'name.tool-goal': 'Goals',
-  'desc.tool-goal': 'Records and tracks the current goal',
   'name.plan-mode': 'Plan mode',
-  'desc.plan-mode': 'Plan before acting',
   'name.compaction-basic': 'Context compaction',
-  'desc.compaction-basic': 'Condenses a long conversation automatically',
   'name.command-compact': 'Compact command',
-  'desc.command-compact': 'The /compact command',
   'name.tool-result-pruner': 'Tool result pruning',
-  'desc.tool-result-pruner': 'Drops stale tool output',
   'name.tool-subagent-control': 'Subagent control',
-  'desc.tool-subagent-control': 'Inspects and manages subagents',
   'name.tool-subagent-list-agents': 'Subagent list',
-  'desc.tool-subagent-list-agents': 'Lists the running subagents',
   'name.tool-subagent': 'Subagent',
-  'desc.tool-subagent': 'Delegates tasks to parallel assistants',
   'name.tool-subagent-fork': 'Fork subagent',
-  'desc.tool-subagent-fork': 'Continues a task from a copy of the current context',
   'name.tool-subagent-codex': 'Codex subagent',
-  'desc.tool-subagent-codex': 'Hands a task to Codex',
   'name.tool-subagent-claude-code': 'Claude Code subagent',
-  'desc.tool-subagent-claude-code': 'Hands a task to Claude Code',
   'name.workflow-ptc': 'Workflow engine',
-  'desc.workflow-ptc': 'Runs multi-step workflows',
   'name.tool-workflow': 'Workflows',
-  'desc.tool-workflow': 'Orchestrates multi-step tasks',
   'name.tool-ralph': 'Ralph loop',
-  'desc.tool-ralph': 'Repeats a task until it is done',
   'name.tool-ask-user': 'Ask the user',
-  'desc.tool-ask-user': 'Asks you when a decision is needed',
   'name.tool-todo': 'To-do',
-  'desc.tool-todo': 'Keeps a task list',
   'name.tool-web': 'Web',
-  'desc.tool-web': 'Searches and fetches web pages',
   'name.tool-cordis': 'Dynamic plugins',
-  'desc.tool-cordis': 'Lets the model define and run plugins',
   'name.tool-presentation': 'Tool presentation',
-  'desc.tool-presentation': 'Shapes how tool results are shown',
   'name.pty': 'Terminal sessions',
-  'desc.pty': 'Keeps a persistent terminal',
   'name.terminal-bash': 'Bash terminal',
-  'desc.terminal-bash': 'A persistent bash session',
   'name.persistent-bash': 'Persistent terminal tool',
-  'desc.persistent-bash': 'Runs commands in one lasting terminal',
   'name.terminal-pwsh': 'PowerShell session',
-  'desc.terminal-pwsh': 'A persistent PowerShell session',
   'name.persistent-pwsh': 'Persistent PowerShell tool',
-  'desc.persistent-pwsh': 'Runs commands in one lasting PowerShell',
   'name.fs-local': 'Local filesystem',
-  'desc.fs-local': 'Accesses files on this machine',
   'name.str-replace-editor': 'Text editor',
-  'desc.str-replace-editor': 'Edits files by replacing snippets',
 } satisfies Record<PluginManagerLocaleKey, string>

+ 19 - 37
packages/client/ui-plugin-manager/src/client/manager-store.ts

@@ -1,7 +1,6 @@
 /**
- * The plugin manager's state, shared by the Manage plugins tab and the
- * capabilities section of a preset's detail page: the Host's package views
- * and preset compositions, the action in flight, the install run, and the
+ * The plugin manager's state: the Host's package views
+ * and global composition, the action in flight, the install run, and the
  * confirmation a destructive action waits on. Every fact comes from the Host — the store
  * re-reads after each action and after every `plugins/changed` event, so a
  * change made on another surface shows here without a manual refresh.
@@ -14,18 +13,10 @@ import type {
   PluginInstallLogChunk,
   PluginInstallRejection,
   PluginInstallResult,
-  PluginInventorySnapshot,
   PluginPackageView,
-  PluginRowTarget,
 } from '@deepseek-ai/dsh-api-remotes/client'
 import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store'
 
-/** One preset composition as the inventory reports it, with its rows. */
-export type PresetGroup = NonNullable<PluginInventorySnapshot['agentPresets']>[number]
-
-/** One row of a preset composition. */
-export type PresetRow = PresetGroup['rows'][number]
-
 /** What the last action left to say. */
 export type ManagerNotice =
   | { readonly kind: 'restart'; readonly packageName: string }
@@ -98,7 +89,6 @@ export interface PluginManagerState {
   /** `unavailable` when the Host runs without a profile runtime; `error` keeps the last packages. */
   readonly status: 'idle' | 'loading' | 'ready' | 'error' | 'unavailable'
   readonly packages: readonly PluginPackageView[]
-  readonly presets: readonly PresetGroup[]
   /** Module names of the rows the host tree carries, for the **Add to…** menu's "added" marks. */
   readonly globalModules: readonly string[]
   /** Package names and row keys with an action crossing the wire. */
@@ -132,14 +122,12 @@ export interface PluginManagerFace {
   confirm: () => void
   cancelConfirm: () => void
   /** Add a row naming one of a package's modules — by its declared name, `.` for the main export — to a user layer. */
-  addRow: (packageName: string, declaredName: string, target: PluginRowTarget) => void
-  removeRow: (target: PluginRowTarget, rowId: string) => void
-  setRowDisabled: (target: PluginRowTarget, rowId: string, disabled: boolean) => void
+  addRow: (packageName: string, declaredName: string) => void
+  removeRow: (rowId: string) => void
+  setRowDisabled: (rowId: string, disabled: boolean) => void
   /** Switch one of a package's rows off in the global layer, after asking when other rows inject what it provides. */
   disableRow: (packageName: string, entryId: string, rowId: string) => void
   dismissNotice: () => void
-  /** Display name for one preset, resolved through the agent-preset dictionaries. */
-  presetName: (preset: PresetGroup) => string
 }
 
 /** A Remote answer as the generated client returns it. */
@@ -179,12 +167,11 @@ function reasonOf(error: { message: string; details?: unknown }): string {
 
 /**
  * The key one row occupies in the busy list.
- * @param target - the layer the row lives in.
  * @param rowId - the row's id in that layer.
  * @returns the busy key.
  */
-export function rowKey(target: PluginRowTarget, rowId: string): string {
-  return `${target.kind === 'global' ? 'global' : `preset:${target.preset}`}:${rowId}`
+export function rowKey(rowId: string): string {
+  return `global:${rowId}`
 }
 
 const IDLE_INSTALL: InstallState = {
@@ -203,14 +190,12 @@ export class PluginManagerController {
   /**
    * @param ctx - the tab plugin's context, whose `remote.plugins` and
    * `remote.pluginInventory` namespaces answer.
-   * @param presetName - display name for one preset.
    */
   constructor(
     private readonly ctx: ClientContext,
-    private readonly presetName: (preset: PresetGroup) => string,
   ) {
     this.store = createSnapshotStore<PluginManagerState>({
-      status: 'idle', packages: [], presets: [], globalModules: [], busy: [], notice: null,
+      status: 'idle', packages: [], globalModules: [], busy: [], notice: null,
       install: IDLE_INSTALL, confirm: null,
     })
   }
@@ -261,24 +246,23 @@ export class PluginManagerController {
       uninstall: (packageName) => { void this.askConfirm('uninstall', packageName) },
       confirm: () => { void this.confirm() },
       cancelConfirm: () => { this.pendingConfirm = undefined; this.patch({ confirm: null }) },
-      addRow: (packageName, declaredName, target) => {
+      addRow: (packageName, declaredName) => {
         void this.run(packageName, { packageName }, async () => {
-          this.answer(await this.ctx.remote.plugins.addRow(packageName, target, { module: declaredName }))
+          this.answer(await this.ctx.remote.plugins.addRow(packageName, { module: declaredName }))
         })
       },
-      removeRow: (target, rowId) => {
-        void this.run(rowKey(target, rowId), { rowId }, async () => {
-          this.answer(await this.ctx.remote.plugins.removeRow(target, rowId))
+      removeRow: (rowId) => {
+        void this.run(rowKey(rowId), { rowId }, async () => {
+          this.answer(await this.ctx.remote.plugins.removeRow(rowId))
         })
       },
-      setRowDisabled: (target, rowId, disabled) => {
-        void this.run(rowKey(target, rowId), { rowId }, async () => {
-          this.answer(await this.ctx.remote.plugins.setRowDisabled(target, rowId, disabled))
+      setRowDisabled: (rowId, disabled) => {
+        void this.run(rowKey(rowId), { rowId }, async () => {
+          this.answer(await this.ctx.remote.plugins.setRowDisabled(rowId, disabled))
         })
       },
       disableRow: (packageName, entryId, rowId) => { void this.askRowConfirm(packageName, entryId, rowId) },
       dismissNotice: () => { this.patch({ notice: null }) },
-      presetName: this.presetName,
     }
   }
 
@@ -303,7 +287,7 @@ export class PluginManagerController {
   }
 
   /**
-   * Read the packages and the preset compositions. A call during an
+   * Read the packages and the global composition. A call during an
    * in-flight read marks one rerun after it settles.
    * @returns settlement after this call's freshness is reflected.
    */
@@ -336,7 +320,6 @@ export class PluginManagerController {
         this.patch({
           status: 'ready',
           packages: packages.value,
-          presets: inventory.ok ? inventory.value.agentPresets ?? [] : this.getSnapshot().presets,
           globalModules: inventory.ok ? inventory.value.entries.map(entry => entry.moduleName) : this.getSnapshot().globalModules,
         })
       } while (this.shouldRerun())
@@ -405,10 +388,9 @@ export class PluginManagerController {
    * once, and only a row something depends on opens the dialog.
    */
   private async askRowConfirm(packageName: string, entryId: string, rowId: string): Promise<void> {
-    const target: PluginRowTarget = { kind: 'global' }
-    const key = rowKey(target, rowId)
+    const key = rowKey(rowId)
     const commit = (): Promise<void> => this.run(key, { rowId }, async () => {
-      this.answer(await this.ctx.remote.plugins.setRowDisabled(target, rowId, true))
+      this.answer(await this.ctx.remote.plugins.setRowDisabled(rowId, true))
     })
     const value = await this.dependentsOf(packageName, key)
     if (value === undefined) return

+ 6 - 68
packages/client/ui-plugin-manager/src/client/presentation.ts

@@ -1,21 +1,13 @@
-/**
- * What the manager's two surfaces say about a package, a preset row, or a
- * failure: the dictionary lookup for harness modules, the package join for
- * installed ones, and the notice copy — shared by the Manage plugins tab and
- * the capabilities section it contributes to a preset's detail page.
- */
+/** Display labels and failure messages for global plugin management. */
 
 import type { PluginPackageView } from '@deepseek-ai/dsh-api-remotes/client'
 import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
 import { zh, type PluginManagerLocaleKey } from './locales.ts'
-import type { ManagerNotice, PresetRow } from './manager-store.ts'
+import type { ManagerNotice } from './manager-store.ts'
 
 /** The translate seat of the manager's dictionary. */
 export type Translate = PropsLocale<'pluginManager'>['t']
 
-/** The scope every harness module is published under. */
-const FIRST_PARTY_SCOPE = '@deepseek-ai/'
-
 /**
  * Compact a package name to what a person calls it.
  * @param name - the package name.
@@ -27,32 +19,11 @@ export function shortName(name: string): string {
 }
 
 /** The dictionary key for one slug, when the dictionary carries it. */
-function copyKey(kind: 'name' | 'desc', slug: string): PluginManagerLocaleKey | undefined {
-  const key = `${kind}.${slug}`
+function copyKey(slug: string): PluginManagerLocaleKey | undefined {
+  const key = `name.${slug}`
   return key in zh ? key as PluginManagerLocaleKey : undefined
 }
 
-/**
- * Display copy of one harness module: by the composition row id first (four
- * subagent rows share one module), then by the unscoped module name without
- * its `dsh-` prefix. Every `name.<slug>` key has its `desc.<slug>` partner.
- * @param t - the manager's translate seat.
- * @param moduleName - the module specifier the row names.
- * @param rowId - the row id the composition declares, when known.
- * @returns the copy, or undefined for a module outside the harness scope or
- * one the dictionary does not name.
- */
-export function harnessCopy(t: Translate, moduleName: string, rowId: string | null): { title: string; description: string } | undefined {
-  if (!moduleName.startsWith(FIRST_PARTY_SCOPE)) return undefined
-  const slugs = [...rowId === null ? [] : [rowId], moduleName.slice(FIRST_PARTY_SCOPE.length).replace(/^dsh-/, '')]
-  for (const slug of slugs) {
-    const name = copyKey('name', slug)
-    if (name === undefined) continue
-    return { title: t(name), description: t(`desc.${slug}` as PluginManagerLocaleKey) }
-  }
-  return undefined
-}
-
 /**
  * The installed package a module belongs to: the package itself or one of its subpaths.
  * @param moduleName - the module specifier.
@@ -63,15 +34,6 @@ export function packageOf(moduleName: string, packages: readonly PluginPackageVi
   return packages.find(pkg => moduleName === pkg.name || moduleName.startsWith(`${pkg.name}/`))
 }
 
-/**
- * The row id a tree entry id ends in: what a user layer addresses.
- * @param entryId - the inventory's entry id, tree-wide under a live mount.
- * @returns the declared row id, or null for a row that declares none.
- */
-export function rowIdOf(entryId: string | null): string | null {
-  return entryId === null ? null : entryId.slice(entryId.lastIndexOf(':') + 1)
-}
-
 /**
  * What a person calls one tree entry: its harness name, else its row id.
  * @param t - the manager's translate seat.
@@ -79,35 +41,11 @@ export function rowIdOf(entryId: string | null): string | null {
  * @returns the label.
  */
 export function rowLabel(t: Translate, entryId: string): string {
-  const id = rowIdOf(entryId) as string
-  const key = copyKey('name', id)
+  const id = entryId.slice(entryId.lastIndexOf(':') + 1)
+  const key = copyKey(id)
   return key === undefined ? id : t(key)
 }
 
-/**
- * What one preset row shows: a title, a one-liner, and whether the person installed it.
- * @param row - the inventory row.
- * @param packages - the profile's packages, which name the installed ones.
- * @param t - the manager's translate seat.
- * @returns the copy.
- */
-export function presetRowCopy(
-  row: PresetRow, packages: readonly PluginPackageView[], t: Translate,
-): { title: string; description?: string; local: boolean } {
-  const pkg = packageOf(row.moduleName, packages)
-  if (pkg !== undefined) {
-    const addable = pkg.addable.find(entry => entry.moduleName === row.moduleName)
-    return {
-      title: addable?.title ?? pkg.title ?? shortName(pkg.name),
-      ...pkg.description === undefined ? {} : { description: pkg.description },
-      local: pkg.installed,
-    }
-  }
-  const harness = harnessCopy(t, row.moduleName, rowIdOf(row.entryId))
-  if (harness !== undefined) return { ...harness, local: false }
-  return { title: shortName(row.moduleName), description: row.moduleName, local: !row.moduleName.startsWith(FIRST_PARTY_SCOPE) }
-}
-
 /**
  * The copy for a refusal every mutation can meet: the manager is busy, or a session is running.
  * @param failure - the Host's code and reason.

+ 0 - 18
packages/client/ui-plugin-manager/tests/browser-plugin.client.spec.tsx

@@ -9,7 +9,6 @@ import { TestRemote, usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-t
 import { apply, inject, NS, PANEL_ID } from '../src/client/index.ts'
 import { PluginManagerPage } from '../src/client/PluginManagerPage.tsx'
 import { PluginsPanelIcon } from '../src/client/PluginsPanelIcon.tsx'
-import { PresetPluginsSection } from '../src/client/PresetPluginsSection.tsx'
 import type { PluginManagerFace } from '../src/client/manager-store.ts'
 import { apply as hostApply } from '../src/index.ts'
 
@@ -42,7 +41,6 @@ function declare(slots: SlotRegistry): () => void {
     children: {
       'main': { kind: 'keyed', scope: 'root' },
       'sidebar.panellist': { kind: 'list', scope: 'root' },
-      'settings.agentPreset.detail': { kind: 'list', scope: 'root' },
     },
   } as never, () => null)
 }
@@ -76,16 +74,7 @@ describe('ui-plugin-manager browser plugin', () => {
     expect(icon.options).toMatchObject({ id: PANEL_ID, order: 0 })
     expect(icon.locale).toBe(NS)
     expect(resolveSlotLabel(icon.options.label)).toBe('插件')
-    // The same store feeds the capabilities section of every preset's detail page.
-    const section = b.slots.entries('settings.agentPreset.detail')[0]!
-    expect(section.component).toBe(PresetPluginsSection)
-    expect(section.options).toMatchObject({ id: 'plugins', order: 0 })
-    expect(section.locale).toBe(NS)
-    const sectionFace = (section.inject as unknown as () => PluginManagerFace)()
-    expect(b.list).not.toHaveBeenCalled()
-
     const face = (entry.inject as unknown as () => PluginManagerFace)()
-    expect(sectionFace.hooks.pluginManager).toBe(face.hooks.pluginManager)
     // A Host change before the first render is not a reason to read.
     b.remote.emit('plugins/changed', [{ reason: 'install' }])
     b.ctx.emit('connection/reset')
@@ -105,16 +94,9 @@ describe('ui-plugin-manager browser plugin', () => {
     b.remote.emit('plugins/install-log', [{ jobId: 'j', argv: ['pnpm', 'add', 'pkg'], cwd: '/p', spec: 'pkg', stream: 'stdout', text: 'early' }])
     expect(face.hooks.pluginManager.getSnapshot().install.runs).toEqual([])
 
-    // Shipped preset names resolve over the agent-preset dictionaries the
-    // real plugin registers; user-authored metadata stays untranslated.
-    b.locale.register('settings.agentPreset', 'zh', { presetStandardName: '标准模式' } as never)
-    expect(face.presetName({ id: 'standard', trust: 'system', isDefault: true, rows: [] })).toBe('标准模式')
-    expect(face.presetName({ id: 'mine', trust: 'user', name: '我自己的', isDefault: false, rows: [] })).toBe('我自己的')
-
     await fiber.dispose()
     expect(b.slots.entries('main')).toHaveLength(0)
     expect(b.slots.entries('sidebar.panellist')).toHaveLength(0)
-    expect(b.slots.entries('settings.agentPreset.detail')).toHaveLength(0)
     b.remote.emit('plugins/changed', [{ reason: 'install' }])
     await Promise.resolve()
     expect(b.list).toHaveBeenCalledTimes(3)

+ 47 - 126
packages/client/ui-plugin-manager/tests/components.client.spec.tsx

@@ -6,8 +6,8 @@ import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
 import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
 import { PluginManagerPage } from '../src/client/PluginManagerPage.tsx'
 import type { PluginManagerPageProps } from '../src/client/PluginManagerPage.tsx'
-import { rowKey, type InstallState, type PluginManagerState, type PresetGroup } from '../src/client/manager-store.ts'
-import { en, zh, type PluginManagerLocaleKey } from '../src/client/locales.ts'
+import { rowKey, type InstallState, type PluginManagerState } from '../src/client/manager-store.ts'
+import { en, type PluginManagerLocaleKey } from '../src/client/locales.ts'
 
 afterEach(cleanup)
 
@@ -34,10 +34,6 @@ function pkg(overrides: Partial<PluginPackageView> = {}): PluginPackageView {
   }
 }
 
-function preset(overrides: Partial<PresetGroup> = {}): PresetGroup {
-  return { id: 'standard', trust: 'system', name: '标准', isDefault: true, rows: [], ...overrides }
-}
-
 const IDLE_INSTALL: InstallState = {
   open: false, spec: '', enable: true, phase: 'idle', runs: [], installed: [], enabled: [], installedOnly: [], plain: [], removed: [], failure: null,
 }
@@ -45,7 +41,6 @@ const IDLE_INSTALL: InstallState = {
 const READY: PluginManagerState = {
   status: 'ready',
   packages: [],
-  presets: [],
   globalModules: [],
   busy: [],
   notice: null,
@@ -77,25 +72,12 @@ function renderTab(state: Partial<PluginManagerState> = {}) {
   const props = {
     t,
     ...actions,
-    presetName: (candidate: PresetGroup) => candidate.name ?? candidate.id,
     usePluginManager: bindSnapshotSelector(store),
   } as unknown as PluginManagerPageProps
   render(<PluginManagerPage {...props} />)
   return { store, actions, set: (next: Partial<PluginManagerState>) => { act(() => { store.set({ ...store.getSnapshot(), ...next }) }) } }
 }
 
-describe('locale dictionaries', () => {
-  it('pair every harness module name with its description in both languages', () => {
-    const names = Object.keys(zh).filter(key => key.startsWith('name.'))
-    expect(names.length).toBeGreaterThan(20)
-    for (const key of names) {
-      const partner = key.replace(/^name\./, 'desc.') as PluginManagerLocaleKey
-      expect(zh[partner]).toBeTypeOf('string')
-      expect(en[partner]).toBeTypeOf('string')
-    }
-  })
-})
-
 describe('PluginManagerPage', () => {
   it('keeps a non-dependency override failure visible even when owned rows are waiting', () => {
     renderTab({ packages: [pkg({
@@ -151,6 +133,11 @@ describe('PluginManagerPage', () => {
     expect(actions.openInstall).toHaveBeenCalledTimes(1)
   })
 
+  it('shows a problem for a plugin with an invalid declaration', () => {
+    renderTab({ packages: [pkg({ kind: 'plugin', status: 'plain', reason: 'invalid declaration' })] })
+    expect(screen.getByText(en.statusProblem)).toBeTruthy()
+  })
+
   it('lists every package as a card, tags only restarts, problems, and built-ins, and names what waits for a restart', () => {
     const { version: _unversioned, ...bundledPackage } = pkg({
       name: '@deepseek-ai/dsh-bundle-first-party', title: 'First party', installed: false,
@@ -334,13 +321,12 @@ describe('PluginManagerPage', () => {
         pkg({ name: '@deepseek-ai/dsh-core', title: 'Core', installed: false, rows: [{ entryId: 'include:core', rowId: 'core', moduleName: '@deepseek-ai/dsh-core', ...userOff }] }),
       ],
     })
-    const target = { kind: 'global' } as const
     fireEvent.click(screen.getByRole('button', { name: 'View better-sidebar' }))
     // A row the person switched off switches back on at once; one the pack itself keeps off is locked and says why.
     const off = screen.getByRole('switch', { name: 'Enable component off' })
     expect(off.getAttribute('aria-checked')).toBe('false')
     fireEvent.click(off)
-    expect(actions.setRowDisabled).toHaveBeenLastCalledWith(target, 'off', false)
+    expect(actions.setRowDisabled).toHaveBeenLastCalledWith('off', false)
     expect(actions.disableRow).not.toHaveBeenCalled()
     const gated = screen.getByRole('switch', { name: 'Enable component gated' })
     expect(gated).toHaveProperty('disabled', true)
@@ -356,7 +342,7 @@ describe('PluginManagerPage', () => {
     expect(actions.disableRow).toHaveBeenLastCalledWith('dsh-better-sidebar', 'include:better-sidebar', 'better-sidebar')
     expect(actions.setRowDisabled).toHaveBeenCalledTimes(1)
     // Only the row with a write in flight goes inert; a busy package takes every row with it.
-    set({ busy: [rowKey(target, 'better-sidebar')] })
+    set({ busy: [rowKey('better-sidebar')] })
     expect(screen.getByRole('switch', { name: 'Enable component better-sidebar' })).toHaveProperty('disabled', true)
     expect(screen.getByRole('switch', { name: 'Enable component off' })).toHaveProperty('disabled', false)
     set({ busy: ['dsh-better-sidebar'] })
@@ -381,107 +367,43 @@ describe('PluginManagerPage', () => {
     }
   })
 
-  it('opens a plugin\'s page with its modules, where each is composed, and an Add to… per module', () => {
-    const { actions } = renderTab({
-      packages: [
-        pkg({
-          name: 'dsh-tool-foo', kind: 'plugin', status: 'plain', title: 'Foo tools', reason: 'one module refused',
-          addable: [
-            { moduleName: 'dsh-tool-foo', declaredName: '.' },
-            { moduleName: 'dsh-tool-foo/bar', declaredName: 'bar', title: 'Bar' },
-            { moduleName: 'dsh-tool-foo/broken', declaredName: 'broken' },
-            { moduleName: 'dsh-tool-foo/silent', declaredName: 'silent' },
-          ],
-        }),
-        pkg({ name: 'dsh-tool-bare', kind: 'plugin', status: 'plain', addable: [{ moduleName: 'dsh-tool-bare', declaredName: '.' }] }),
-      ],
-      presets: [
-        preset({ rows: [{ entryId: 'preset:foo', moduleName: 'dsh-tool-foo', source: 'user', enabled: true, fiberPhase: null }] }),
-        preset({ id: 'other', name: 'Other', isDefault: false }),
-      ],
-      globalModules: ['dsh-tool-foo'],
-    })
-    fireEvent.click(screen.getByRole('button', { name: 'View Foo tools' }))
-    expect(screen.getByText(en.statusProblem)).toBeTruthy()
-    expect(screen.getByText(`${en.reasonLabel}: one module refused`)).toBeTruthy()
-    // The main export reads by the package title; a module by its own title;
-    // each names where it is composed, or why it cannot be.
-    const moduleText = (name: string): string | undefined => document.querySelector(`[data-plugin-module="${name}"]`)?.textContent
-    expect(moduleText('dsh-tool-foo')).toBe(`Foo toolsdsh-tool-foo${en.moduleJoined.replace('{targets}', `${en.joinedGlobal}, 标准`)}${en.addTo}`)
-    expect(moduleText('dsh-tool-foo/bar')).toBe(`Bardsh-tool-foo/bar${en.moduleNotJoined}${en.addTo}`)
-    expect(document.querySelector('[data-plugin-module="dsh-tool-foo/broken"]')?.getAttribute('data-state')).toBeNull()
-    expect(moduleText('dsh-tool-foo/broken')).toContain(en.moduleNotJoined)
-    expect(moduleText('dsh-tool-foo/silent')).toContain(en.moduleNotJoined)
-    // Every declared module offers Add to…; targets already joined stay marked.
-    const menus = screen.getAllByRole('button', { name: en.addTo })
-    expect(menus).toHaveLength(4)
-    fireEvent.click(menus[0] as HTMLElement)
-    expect(screen.getByRole('menuitem', { name: en.addToGlobalAdded })).toBeTruthy()
-    expect(screen.getByRole('menuitem', { name: en.addToPresetAdded.replace('{name}', '标准') })).toBeTruthy()
-    fireEvent.click(screen.getByRole('menuitem', { name: en.addToPreset.replace('{name}', 'Other') }))
-    expect(actions.addRow).toHaveBeenLastCalledWith('dsh-tool-foo', '.', { kind: 'preset', preset: 'other' })
-    fireEvent.click(menus[1] as HTMLElement)
-    fireEvent.click(screen.getByRole('menuitem', { name: en.addToGlobal }))
-    expect(actions.addRow).toHaveBeenLastCalledWith('dsh-tool-foo', 'bar', { kind: 'global' })
-    // The anchor closes its own menu; so does Escape.
-    fireEvent.click(menus[1] as HTMLElement)
-    fireEvent.click(menus[1] as HTMLElement)
-    expect(screen.queryByRole('menuitem', { name: en.addToGlobal })).toBeNull()
-    fireEvent.click(menus[1] as HTMLElement)
-    fireEvent.keyDown(document, { key: 'Escape' })
-    expect(screen.queryByRole('menuitem', { name: en.addToGlobal })).toBeNull()
-    // A package without a title reads by its short name.
-    fireEvent.click(screen.getByRole('button', { name: en.backToList }))
-    fireEvent.click(screen.getByRole('button', { name: 'View tool-bare' }))
-    expect(moduleText('dsh-tool-bare')).toContain(`tool-baredsh-tool-bare${en.moduleNotJoined}`)
+  it('adds a declared module globally and disables the action once it is present', () => {
+    const mod = { moduleName: 'example/search', declaredName: './search', title: 'Search' }
+    const { actions, set } = renderTab({ packages: [pkg({ name: 'example', kind: 'plugin', status: 'plain', addable: [mod] })] })
+    fireEvent.click(screen.getByRole('button', { name: en.addToGlobal }))
+    expect(actions.addRow).toHaveBeenCalledWith('example', './search')
+    set({ globalModules: ['example/search'] })
+    expect(screen.getByRole('button', { name: en.addToGlobalAdded })).toHaveProperty('disabled', true)
+    fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', 'example') }))
+    expect(screen.getByText(en.joinedGlobal)).toBeTruthy()
+    expect(screen.getByRole('button', { name: en.addToGlobalAdded })).toHaveProperty('disabled', true)
   })
 
-  it('offers Add to… for a plugin with importable modules, marking the targets it already joined', () => {
-    const { actions } = renderTab({
-      packages: [
-        pkg({ name: 'dsh-tool-foo', kind: 'plugin', status: 'plain', addable: [{ moduleName: 'dsh-tool-foo', declaredName: '.' }] }),
-        pkg({
-          name: 'multi',
-          kind: 'plugin',
-          status: 'plain',
-          addable: [
-            { moduleName: 'multi/a', declaredName: './a', title: 'A tool' },
-            { moduleName: 'multi/b', declaredName: './b' },
-            { moduleName: 'multi/c', declaredName: './c' },
-          ],
-        }),
-        pkg({ name: 'unloadable', kind: 'plugin', status: 'plain', reason: 'the package failed to import: x' }),
-      ],
-      presets: [
-        preset({ rows: [{ entryId: 'fixture', moduleName: 'dsh-tool-foo', enabled: true, fiberPhase: null, source: 'user' }] }),
-        preset({ id: 'research', trust: 'user', name: 'Research', isDefault: false }),
-        preset({ id: 'broken', trust: 'user', name: 'Broken', isDefault: false, broken: 'bad yaml' }),
-      ],
-      globalModules: ['multi/a'],
-    })
-    // A plugin whose import failed offers nothing and reads as a problem.
-    expect(screen.getAllByRole('button', { name: en.addTo })).toHaveLength(2)
-    expect(screen.getByText(en.statusProblem)).toBeTruthy()
+  it('selects among multiple declared modules and adds one directly to the global layer', () => {
+    const { actions } = renderTab({ packages: [pkg({ name: 'example', kind: 'plugin', status: 'plain', addable: [
+      { moduleName: 'example/search', declaredName: './search', title: 'Search' },
+      { moduleName: 'example/fetch', declaredName: './fetch', title: 'Fetch' },
+    ] })] })
+    fireEvent.click(screen.getByRole('button', { name: en.addToGlobal }))
+    fireEvent.click(screen.getByRole('menuitem', { name: 'Fetch' }))
+    expect(actions.addRow).toHaveBeenCalledWith('example', './fetch')
+  })
 
-    const [foo, multi] = screen.getAllByRole('button', { name: en.addTo })
-    fireEvent.click(foo!)
-    expect(screen.getByRole('menuitem', { name: en.addToGlobal })).toBeTruthy()
-    expect(screen.getByRole('menuitem', { name: 'Preset: 标准 (added)' })).toBeTruthy()
-    expect(screen.getByRole('menuitem', { name: 'Preset: Broken' })).toBeTruthy()
-    fireEvent.click(screen.getByRole('menuitem', { name: 'Preset: Research' }))
-    expect(actions.addRow).toHaveBeenCalledWith('dsh-tool-foo', '.', { kind: 'preset', preset: 'research' })
-    fireEvent.click(foo!)
-    fireEvent.click(screen.getByRole('menuitem', { name: en.addToGlobal }))
-    expect(actions.addRow).toHaveBeenLastCalledWith('dsh-tool-foo', '.', { kind: 'global' })
-    fireEvent.click(foo!)
+  it('adds untitled main and subpath modules from a package detail page', () => {
+    const { actions, set } = renderTab({ packages: [pkg({ name: '@fixture/modules', kind: 'plugin', status: 'plain', addable: [
+      { moduleName: '@fixture/modules', declaredName: '.' },
+      { moduleName: '@fixture/modules/search', declaredName: './search' },
+    ] })] })
+    fireEvent.click(screen.getByRole('button', { name: en.addToGlobal }))
+    expect(screen.getByRole('menuitem', { name: './search' })).toBeTruthy()
     fireEvent.keyDown(document, { key: 'Escape' })
-    expect(screen.queryByRole('menuitem')).toBeNull()
-
-    // Several importable modules nest their targets under the module.
-    fireEvent.click(multi!)
-    expect(screen.getByText('A tool')).toBeTruthy()
-    expect(screen.getByText('./b')).toBeTruthy()
-    expect(screen.getByText('./c')).toBeTruthy()
+    expect(screen.queryByRole('menu')).toBeNull()
+    fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', 'modules') }))
+    const buttons = screen.getAllByRole('button', { name: en.addToGlobal })
+    fireEvent.click(buttons[1]!)
+    expect(actions.addRow).toHaveBeenCalledWith('@fixture/modules', './search')
+    set({ busy: ['@fixture/modules'] })
+    expect(screen.getAllByRole('button', { name: en.addToGlobal }).every(button => (button as HTMLButtonElement).disabled)).toBe(true)
   })
 
   it('drives the install dialog through its phases and words each outcome', () => {
@@ -598,7 +520,6 @@ describe('PluginManagerPage', () => {
   it('names what still uses a package in the confirmation and runs the action through it', () => {
     const { actions, set } = renderTab({
       packages: [pkg(), pkg({ name: 'dsh-other', title: 'Other' })],
-      presets: [preset()],
       confirm: { action: 'uninstall', packageName: 'dsh-better-sidebar', dependents: undefined },
     })
     // An uninstall is always confirmed, so its dialog opens while the Host is still asked.
@@ -613,17 +534,17 @@ describe('PluginManagerPage', () => {
         dependents: {
           services: [{ service: 'sidebar', providedBy: 'include:sidebar', injectedBy: ['include:tool-bash', 'include:custom'] }],
           references: [
-            { target: { kind: 'global' }, rowId: 'r1', moduleName: 'dsh-other/x' },
-            { target: { kind: 'preset', preset: 'standard' }, rowId: 'r2', moduleName: '@fixture/other' },
-            { target: { kind: 'preset', preset: 'ghost' }, rowId: 'r3', moduleName: 'dsh-better-sidebar' },
+            { rowId: 'r1', moduleName: 'dsh-other/x' },
+            { rowId: 'r2', moduleName: '@fixture/other' },
+            { rowId: 'r3', moduleName: 'dsh-better-sidebar' },
           ],
         },
       },
     })
     expect(screen.getByText(en.dependentService.replace('{rows}', `${en['name.tool-bash']}, custom`))).toBeTruthy()
     expect(screen.getByText(en.dependentReferenceGlobal.replace('{row}', 'Other'))).toBeTruthy()
-    expect(screen.getByText(en.dependentReferencePreset.replace('{name}', '标准').replace('{row}', 'r2'))).toBeTruthy()
-    expect(screen.getByText(en.dependentReferencePreset.replace('{name}', 'ghost').replace('{row}', 'better-sidebar'))).toBeTruthy()
+    expect(screen.getByText(en.dependentReferenceGlobal.replace('{row}', 'r2'))).toBeTruthy()
+    expect(screen.getByText(en.dependentReferenceGlobal.replace('{row}', 'better-sidebar'))).toBeTruthy()
     fireEvent.click(screen.getByRole('button', { name: en.confirmDisable }))
     expect(actions.confirm).toHaveBeenCalledTimes(1)
     fireEvent.click(screen.getByRole('button', { name: en.cancel }))

+ 23 - 33
packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts

@@ -6,7 +6,7 @@
 import { describe, expect, it, vi } from 'vitest'
 import type { PluginPackageView } from '@deepseek-ai/dsh-api-remotes/client'
 import { RemoteError } from '@deepseek-ai/dsh-client-test-runtime'
-import { PluginManagerController, rowKey, type PresetGroup } from '../src/client/manager-store.ts'
+import { PluginManagerController, rowKey } from '../src/client/manager-store.ts'
 
 const BUNDLE: PluginPackageView = {
   name: 'dsh-better-sidebar',
@@ -22,13 +22,6 @@ const BUNDLE: PluginPackageView = {
   liveReload: true,
 }
 
-const STANDARD: PresetGroup = {
-  id: 'standard',
-  trust: 'system',
-  isDefault: true,
-  rows: [{ entryId: 'bash', moduleName: '@deepseek-ai/dsh-tool-bash', enabled: true, fiberPhase: null, source: 'preset' }],
-}
-
 /** One host-tree row the inventory lists, as far as the store reads it. */
 const GLOBAL_ENTRY = {
   entryId: 'include:ui-settings', moduleName: '@deepseek-ai/dsh-client-ui-settings', enabled: true, fiberPhase: 'active', trust: 'builtin',
@@ -67,21 +60,21 @@ function bench(overrides: Partial<Record<string, ReturnType<typeof vi.fn>>> = {}
     enable: vi.fn(() => Promise.resolve(ok({ changed: true, effect: 'live' }))),
     disable: vi.fn(() => Promise.resolve(ok({ changed: true, effect: 'restart' }))),
     retry: vi.fn(() => Promise.resolve(ok({ changed: true, effect: 'live' }))),
-    addRow: vi.fn(() => Promise.resolve(ok({ target: { kind: 'global' }, rowId: 'r', file: '/f' }))),
+    addRow: vi.fn(() => Promise.resolve(ok({ rowId: 'r', file: '/f' }))),
     removeRow: vi.fn(() => Promise.resolve(ok(undefined))),
     setRowDisabled: vi.fn(() => Promise.resolve(ok(undefined))),
     dependents: vi.fn(() => Promise.resolve(ok({ services: [], references: [] }))),
     ...overrides,
   }
-  const inventory = { list: vi.fn(() => Promise.resolve(ok({ entries: [GLOBAL_ENTRY], agentPresets: [STANDARD] }))) }
+  const inventory = { list: vi.fn(() => Promise.resolve(ok({ entries: [GLOBAL_ENTRY] }))) }
   const ctx = { remote: { plugins, pluginInventory: inventory } } as never
-  const controller = new PluginManagerController(ctx, preset => preset.name ?? preset.id)
+  const controller = new PluginManagerController(ctx)
   const face = controller.inject()
   return { plugins, inventory, controller, face, state: () => controller.getSnapshot() }
 }
 
 describe('PluginManagerController', () => {
-  it('starts idle, reads packages and presets on first use, and folds concurrent loads', async () => {
+  it('starts idle, reads packages and global modules on first use, and folds concurrent loads', async () => {
     const gate = deferred<ReturnType<typeof ok<PluginPackageView[]>>>()
     const { plugins, face, state, controller } = bench({ list: vi.fn().mockReturnValueOnce(gate.promise).mockResolvedValue(ok([BUNDLE])) })
     expect(state().status).toBe('idle')
@@ -94,13 +87,12 @@ describe('PluginManagerController', () => {
     await mid
     // The in-flight read reran once for the load that landed mid-read.
     expect(plugins.list).toHaveBeenCalledTimes(2)
-    expect(state()).toMatchObject({ status: 'ready', packages: [BUNDLE], presets: [STANDARD], globalModules: [GLOBAL_ENTRY.moduleName] })
+    expect(state()).toMatchObject({ status: 'ready', packages: [BUNDLE], globalModules: [GLOBAL_ENTRY.moduleName] })
     face.ensure()
     expect(plugins.list).toHaveBeenCalledTimes(2)
     face.refresh()
     await controller.load()
     expect(plugins.list).toHaveBeenCalledTimes(3)
-    expect(face.presetName(STANDARD)).toBe('standard')
   })
 
   it('reports an unavailable profile runtime and keeps the last packages across a failed read', async () => {
@@ -119,12 +111,11 @@ describe('PluginManagerController', () => {
     expect(plugins.list).toHaveBeenCalledTimes(3)
   })
 
-  it('keeps the held presets and host-tree modules when the inventory read is refused', async () => {
+  it('keeps the held host-tree modules when the inventory read is refused', async () => {
     const { inventory, controller, state } = bench()
     await controller.load()
     inventory.list.mockResolvedValueOnce(refused('gateway/internal', 'boom') as never)
     await controller.load()
-    expect(state().presets).toEqual([STANDARD])
     expect(state().globalModules).toEqual([GLOBAL_ENTRY.moduleName])
   })
 
@@ -193,7 +184,7 @@ describe('PluginManagerController', () => {
     await controller.load()
     face.disableRow(BUNDLE.name, 'include:seam', 'seam')
     // The row is inert while the Host is asked, and no dialog opens before the answer.
-    expect(state().busy).toEqual([rowKey({ kind: 'global' }, 'seam')])
+    expect(state().busy).toEqual([rowKey('seam')])
     expect(state().confirm).toBeNull()
     // Of the package's dependents, only the services this row provides are the row's.
     await vi.waitFor(() => {
@@ -201,20 +192,20 @@ describe('PluginManagerController', () => {
     })
     expect(plugins.setRowDisabled).not.toHaveBeenCalled()
     face.confirm()
-    await vi.waitFor(() => { expect(plugins.setRowDisabled).toHaveBeenCalledWith({ kind: 'global' }, 'seam', true) })
+    await vi.waitFor(() => { expect(plugins.setRowDisabled).toHaveBeenCalledWith('seam', true) })
     await vi.waitFor(() => { expect(state().busy).toEqual([]) })
     // A row nothing depends on switches off without asking; so does one whose dependents the Host refused to name.
     face.disableRow(BUNDLE.name, 'include:lonely', 'lonely')
-    await vi.waitFor(() => { expect(plugins.setRowDisabled).toHaveBeenCalledWith({ kind: 'global' }, 'lonely', true) })
+    await vi.waitFor(() => { expect(plugins.setRowDisabled).toHaveBeenCalledWith('lonely', true) })
     expect(state().confirm).toBeNull()
     plugins.dependents.mockResolvedValueOnce(refused('gateway/internal', 'boom') as never)
     face.disableRow(BUNDLE.name, 'include:unknown', 'unknown')
-    await vi.waitFor(() => { expect(plugins.setRowDisabled).toHaveBeenCalledWith({ kind: 'global' }, 'unknown', true) })
+    await vi.waitFor(() => { expect(plugins.setRowDisabled).toHaveBeenCalledWith('unknown', true) })
     expect(state().confirm).toBeNull()
     // A check that fails on the wire does not stand between the person and the switch.
     plugins.dependents.mockRejectedValueOnce(new Error('down'))
     face.disableRow(BUNDLE.name, 'include:offline', 'offline')
-    await vi.waitFor(() => { expect(plugins.setRowDisabled).toHaveBeenCalledWith({ kind: 'global' }, 'offline', true) })
+    await vi.waitFor(() => { expect(plugins.setRowDisabled).toHaveBeenCalledWith('offline', true) })
     expect(state().confirm).toBeNull()
   })
 
@@ -280,39 +271,38 @@ describe('PluginManagerController', () => {
     await vi.waitFor(() => { expect(state().busy).toEqual([]) })
     expect(state().notice).toBeNull()
 
-    face.addRow('@fixture/tool', '.', { kind: 'preset', preset: 'standard' })
+    face.addRow('@fixture/tool', '.')
     await vi.waitFor(() => {
-      expect(plugins.addRow).toHaveBeenCalledWith('@fixture/tool', { kind: 'preset', preset: 'standard' }, { module: '.' })
+      expect(plugins.addRow).toHaveBeenCalledWith('@fixture/tool', { module: '.' })
     })
     await vi.waitFor(() => { expect(state().busy).toEqual([]) })
     expect(state().notice).toBeNull()
 
-    const target = { kind: 'preset', preset: 'standard' } as const
-    face.setRowDisabled(target, 'bash', true)
-    expect(state().busy).toEqual([rowKey(target, 'bash')])
-    await vi.waitFor(() => { expect(plugins.setRowDisabled).toHaveBeenCalledWith(target, 'bash', true) })
+    face.setRowDisabled('bash', true)
+    expect(state().busy).toEqual([rowKey('bash')])
+    await vi.waitFor(() => { expect(plugins.setRowDisabled).toHaveBeenCalledWith('bash', true) })
     await vi.waitFor(() => { expect(state().busy).toEqual([]) })
-    face.removeRow(target, 'extra')
-    await vi.waitFor(() => { expect(plugins.removeRow).toHaveBeenCalledWith(target, 'extra') })
+    face.removeRow('extra')
+    await vi.waitFor(() => { expect(plugins.removeRow).toHaveBeenCalledWith('extra') })
     await vi.waitFor(() => { expect(state().busy).toEqual([]) })
   })
 
   it('reports a row conflict with the row id and a thrown transport failure generically', async () => {
     const { face, state, controller } = bench({
-      addRow: vi.fn(() => Promise.resolve(refused('plugins/row-conflict', 'taken', { rowId: 'r', target: { kind: 'global' } }))),
+      addRow: vi.fn(() => Promise.resolve(refused('plugins/row-conflict', 'taken', { rowId: 'r' }))),
       removeRow: vi.fn(() => Promise.reject(new Error('offline'))),
       // A rejection that is not an Error reaches the notice by its string form.
       setRowDisabled: vi.fn().mockRejectedValueOnce('plain text'),
     })
     await controller.load()
-    face.addRow('p', 'p', { kind: 'global' })
+    face.addRow('p', 'p')
     await vi.waitFor(() => { expect(state().notice).toMatchObject({ kind: 'failed' }) })
     expect(state().notice).toEqual({ kind: 'failed', code: 'plugins/row-conflict', reason: 'taken', packageName: 'p' })
-    face.removeRow({ kind: 'global' }, 'r')
+    face.removeRow('r')
     await vi.waitFor(() => {
       expect(state().notice).toEqual({ kind: 'failed', code: 'gateway/internal', reason: 'offline', rowId: 'r' })
     })
-    face.setRowDisabled({ kind: 'global' }, 'r', true)
+    face.setRowDisabled('r', true)
     await vi.waitFor(() => { expect(state().notice).toMatchObject({ reason: 'plain text' }) })
   })
 

+ 0 - 209
packages/client/ui-plugin-manager/tests/preset-section.client.spec.tsx

@@ -1,209 +0,0 @@
-// @vitest-environment jsdom
-/**
- * The capabilities section of a preset's detail page: rows named through
- * the dictionary or the package manifest, one switch each, delete for the
- * rows the person added, an add menu over what the preset does not carry
- * yet, and the store's read states and notices.
- */
-
-import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
-import { afterEach, describe, expect, it, vi } from 'vitest'
-import type { PluginPackageView } from '@deepseek-ai/dsh-api-remotes/client'
-import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
-import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
-import { PresetPluginsSection } from '../src/client/PresetPluginsSection.tsx'
-import type { PresetPluginsSectionProps } from '../src/client/PresetPluginsSection.tsx'
-import { rowKey, type PluginManagerState, type PresetGroup } from '../src/client/manager-store.ts'
-import { en, type PluginManagerLocaleKey } from '../src/client/locales.ts'
-
-afterEach(cleanup)
-
-const t = ((key: PluginManagerLocaleKey, params?: Record<string, string>): string =>
-  Object.entries(params ?? {}).reduce(
-    (text, [name, value]) => text.replaceAll(`{${name}}`, value),
-    en[key],
-  )) as PresetPluginsSectionProps['t']
-
-function pkg(overrides: Partial<PluginPackageView> = {}): PluginPackageView {
-  return {
-    name: 'dsh-tool-foo',
-    version: '0.1.0',
-    kind: 'plugin',
-    installed: true,
-    enabled: false,
-    status: 'plain',
-    cordisSameCopy: true,
-    rows: [],
-    overrides: [],
-    addable: [{ moduleName: 'dsh-tool-foo', declaredName: '.' }],
-    liveReload: true,
-    ...overrides,
-  }
-}
-
-function preset(overrides: Partial<PresetGroup> = {}): PresetGroup {
-  return { id: 'standard', trust: 'system', name: '标准', isDefault: true, rows: [], ...overrides }
-}
-
-const READY: PluginManagerState = {
-  status: 'ready',
-  packages: [],
-  presets: [],
-  globalModules: [],
-  busy: [],
-  notice: null,
-  install: { open: false, spec: '', enable: true, phase: 'idle', runs: [], installed: [], enabled: [], installedOnly: [], plain: [], removed: [], failure: null },
-  confirm: null,
-}
-
-const TARGET = { kind: 'preset', preset: 'standard' } as const
-
-function renderSection(state: Partial<PluginManagerState> = {}, presetId = 'standard') {
-  const store = createSnapshotStore<PluginManagerState>({ ...READY, ...state })
-  const actions = {
-    ensure: vi.fn(),
-    addRow: vi.fn(),
-    removeRow: vi.fn(),
-    setRowDisabled: vi.fn(),
-    dismissNotice: vi.fn(),
-  }
-  const props = {
-    t,
-    ...actions,
-    presetId,
-    presetName: '标准',
-    usePluginManager: bindSnapshotSelector(store),
-  } as unknown as PresetPluginsSectionProps
-  render(<PresetPluginsSection {...props} />)
-  return { actions, set: (next: Partial<PluginManagerState>) => { act(() => { store.set({ ...store.getSnapshot(), ...next }) }) } }
-}
-
-describe('PresetPluginsSection', () => {
-  it('asks the store once mounted and renders the read states, a missing preset, and a broken one', () => {
-    const { actions, set } = renderSection({ status: 'loading' })
-    expect(actions.ensure).toHaveBeenCalledTimes(1)
-    expect(screen.getByText(en.loading)).toBeTruthy()
-    expect(screen.queryByRole('button', { name: 'Add a capability to 标准' })).toBeNull()
-
-    set({ status: 'unavailable' })
-    expect(screen.getByRole('status').textContent).toBe(en.unavailable)
-
-    set({ status: 'error' })
-    expect(screen.getByRole('alert').textContent).toBe(en.error)
-    expect(screen.getByText(en.presetMissing)).toBeTruthy()
-
-    set({ status: 'ready', presets: [preset({ broken: 'bad yaml' })] })
-    expect(screen.getByRole('alert').textContent).toBe('bad yaml')
-    expect(screen.queryByText(en.presetMissing)).toBeNull()
-    expect(screen.queryByRole('button', { name: 'Add a capability to 标准' })).toBeNull()
-
-    set({ presets: [preset()] })
-    expect(screen.getByText(en.presetRowsEmpty)).toBeTruthy()
-  })
-
-  it('renders the rows as cards with harness names, local marks, switches, and delete for the rows the person added', () => {
-    const rows: PresetGroup['rows'] = [
-      { entryId: 'include:agent-presets:persona', moduleName: '@deepseek-ai/dsh-persona', enabled: true, fiberPhase: 'active', source: 'preset' },
-      { entryId: 'include:agent-presets:tool-subagent-fork', moduleName: '@deepseek-ai/dsh-tool-subagent', enabled: true, fiberPhase: null, source: 'preset' },
-      { entryId: 'nameless', moduleName: '@deepseek-ai/dsh-unknown-thing', enabled: true, fiberPhase: null, source: 'preset' },
-      { entryId: 'extra', moduleName: '@fixture/extra', enabled: false, fiberPhase: null, source: 'user', disabledBy: 'user' },
-      { entryId: 'gated', moduleName: '@fixture/gated', enabled: false, fiberPhase: null, source: 'preset', disabledBy: 'composition' },
-      { entryId: 'pwsh', moduleName: '@deepseek-ai/dsh-tool-pwsh', enabled: 'conditional', fiberPhase: null, source: 'preset' },
-      { entryId: 'crashy', moduleName: '@fixture/crashy', enabled: true, fiberPhase: 'failed', source: 'preset' },
-      { entryId: null, moduleName: '@deepseek-ai/dsh-tool-todo', enabled: true, fiberPhase: null, source: 'preset' },
-      { entryId: 'foo', moduleName: 'dsh-tool-foo', enabled: true, fiberPhase: null, source: 'user' },
-      { entryId: 'sub', moduleName: 'multi/a', enabled: true, fiberPhase: null, source: 'user' },
-    ]
-    const { actions, set } = renderSection({
-      packages: [
-        pkg({ title: 'Foo tool', description: 'A foo.' }),
-        pkg({ name: '@fixture/extra', addable: [] }),
-        pkg({ name: 'multi', addable: [{ moduleName: 'multi/a', declaredName: './a', title: 'A tool' }] }),
-      ],
-      presets: [preset({ rows })],
-    })
-    // Harness modules read by their dictionary names: the row id first, then the module.
-    expect(screen.getByText(en['name.persona'])).toBeTruthy()
-    expect(screen.getByText(en['desc.persona'])).toBeTruthy()
-    expect(screen.getByText(en['name.tool-subagent-fork'])).toBeTruthy()
-    expect(screen.getByText(en['name.tool-todo'])).toBeTruthy()
-    expect(screen.getByText('unknown-thing')).toBeTruthy()
-    expect(screen.getByText('@deepseek-ai/dsh-unknown-thing')).toBeTruthy()
-    // Installed packages read by their manifest title, an addable module by its own.
-    expect(screen.getByText('Foo tool')).toBeTruthy()
-    expect(screen.getByText('A foo.')).toBeTruthy()
-    expect(screen.getByText('A tool')).toBeTruthy()
-    expect(screen.getAllByText(en.localTag)).toHaveLength(5)
-    expect(screen.getByText(en.rowStateFailed)).toBeTruthy()
-    expect(document.querySelectorAll('[data-preset-row] [data-tone]')).toHaveLength(6)
-    expect(screen.getByText(en.rowNoId)).toBeTruthy()
-    expect(screen.queryByRole('switch', { name: `Enable ${en['name.tool-todo']}` })).toBeNull()
-    expect(screen.getByRole('switch', { name: `Enable ${en['name.tool-pwsh']}` }).getAttribute('aria-checked')).toBe('true')
-
-    fireEvent.click(screen.getByRole('switch', { name: `Enable ${en['name.persona']}` }))
-    expect(actions.setRowDisabled).toHaveBeenCalledWith(TARGET, 'persona', true)
-    const extra = screen.getByRole('switch', { name: 'Enable extra' })
-    expect(extra.getAttribute('aria-checked')).toBe('false')
-    fireEvent.click(extra)
-    expect(actions.setRowDisabled).toHaveBeenLastCalledWith(TARGET, 'extra', false)
-    const gated = screen.getByRole('switch', { name: 'Enable gated' }) as HTMLButtonElement
-    expect(gated.disabled).toBe(true)
-    expect(gated.title).toBe(en.rowLockedByComposition)
-    fireEvent.click(screen.getByRole('button', { name: 'Delete extra from this preset' }))
-    expect(actions.removeRow).toHaveBeenCalledWith(TARGET, 'extra')
-    expect(screen.queryByRole('button', { name: `Delete ${en['name.persona']} from this preset` })).toBeNull()
-
-    set({ busy: [rowKey(TARGET, 'persona')] })
-    expect(screen.getByRole('switch', { name: `Enable ${en['name.persona']}` })).toHaveProperty('disabled', true)
-  })
-
-  it('offers to add the installed modules the preset does not carry yet', () => {
-    const { actions, set } = renderSection({
-      packages: [
-        pkg({ title: 'Foo tool' }),
-        pkg({
-          name: 'multi',
-          title: 'Multi',
-          addable: [
-            { moduleName: 'multi/a', declaredName: './a', title: 'A tool' },
-            { moduleName: 'multi/b', declaredName: './b' },
-            { moduleName: 'multi/c', declaredName: './c' },
-          ],
-        }),
-      ],
-      presets: [preset({ rows: [{ entryId: 'sub', moduleName: 'multi/a', enabled: true, fiberPhase: null, source: 'user' }] })],
-    })
-    const add = screen.getByRole('button', { name: 'Add a capability to 标准' })
-    fireEvent.click(add)
-    // Declared modules remain addable until this preset already carries them.
-    expect(screen.getByRole('menuitem', { name: 'Foo tool' })).toBeTruthy()
-    expect(screen.getByRole('menuitem', { name: 'Multi · ./b' })).toBeTruthy()
-    expect(screen.queryByRole('menuitem', { name: 'A tool' })).toBeNull()
-    expect(screen.getAllByRole('menuitem')).toHaveLength(3)
-    fireEvent.click(screen.getByRole('menuitem', { name: 'Multi · ./b' }))
-    expect(actions.addRow).toHaveBeenCalledWith('multi', './b', TARGET)
-    expect(screen.queryByRole('menuitem')).toBeNull()
-
-    // Nothing left to add reads as such; an action in flight keeps the menu shut.
-    set({ presets: [preset({ rows: [
-      { entryId: 'sub', moduleName: 'multi/a', enabled: true, fiberPhase: null, source: 'user' },
-      { entryId: 'b', moduleName: 'multi/b', enabled: true, fiberPhase: null, source: 'user' },
-      { entryId: 'c', moduleName: 'multi/c', enabled: true, fiberPhase: null, source: 'user' },
-      { entryId: 'foo', moduleName: 'dsh-tool-foo', enabled: true, fiberPhase: null, source: 'user' },
-    ] })] })
-    fireEvent.click(add)
-    expect(screen.getByRole('menuitem', { name: en.capabilitiesAddEmpty })).toBeTruthy()
-    fireEvent.keyDown(document, { key: 'Escape' })
-    set({ busy: ['multi'] })
-    expect(add).toHaveProperty('disabled', true)
-  })
-
-  it('shows the last action\'s outcome and dismisses it', () => {
-    const { actions, set } = renderSection({ presets: [preset()], notice: { kind: 'restart', packageName: 'x' } })
-    expect(screen.getByRole('status').textContent).toContain(en.restartNotice)
-    set({ notice: { kind: 'failed', code: 'plugins/row-conflict', reason: 'r', rowId: 'row-1' } })
-    expect(screen.getByRole('alert').textContent).toContain(en.rowConflict.replace('{row}', 'row-1'))
-    fireEvent.click(screen.getByRole('button', { name: en.dismiss }))
-    expect(actions.dismissNotice).toHaveBeenCalledTimes(1)
-  })
-})

+ 33 - 13
packages/client/ui-plugin-manager/tsconfig.json

@@ -8,18 +8,38 @@
     "src"
   ],
   "references": [
-    { "path": "../../../vendor/cordis" },
-    { "path": "../../api/remotes/tsconfig.client.json" },
-    { "path": "../../host/plugin-manager" },
-    { "path": "../../preset/agent-presets" },
-    { "path": "../../test-support/client-runtime" },
-    { "path": "../locale" },
-    { "path": "../store" },
-    { "path": "../ui-agent-preset" },
-    { "path": "../ui-layout" },
-    { "path": "../ui-primitives" },
-    { "path": "../ui-renderer" },
-    { "path": "../ui-sidebar" },
-    { "path": "../ui-slots" }
+    {
+      "path": "../../../vendor/cordis"
+    },
+    {
+      "path": "../../api/remotes/tsconfig.client.json"
+    },
+    {
+      "path": "../../host/plugin-manager"
+    },
+    {
+      "path": "../../test-support/client-runtime"
+    },
+    {
+      "path": "../locale"
+    },
+    {
+      "path": "../store"
+    },
+    {
+      "path": "../ui-layout"
+    },
+    {
+      "path": "../ui-primitives"
+    },
+    {
+      "path": "../ui-renderer"
+    },
+    {
+      "path": "../ui-sidebar"
+    },
+    {
+      "path": "../ui-slots"
+    }
   ]
 }

+ 1 - 2
packages/client/ui-settings-general/tests/apply.client.spec.ts

@@ -39,7 +39,6 @@ const SEATS = [
 function localeView(preference: string, revision = 0): SettingsNamespaceView {
   return {
     ns: LOCALE_SETTINGS_NAMESPACE,
-    registered: true,
     // The Remote wire serializes nested Schema values before the client rehydrates them.
     schema: JSON.parse(JSON.stringify(LocaleSettingsSchema.toJSON())) as SettingsNamespaceView['schema'],
     value: { preference },
@@ -51,7 +50,7 @@ function localeView(preference: string, revision = 0): SettingsNamespaceView {
 
 async function client(mock: RemoteMock, start: () => Promise<TestClient>, hasDocument = false) {
   const settings = mock.remote.settings
-  settings.describe.mockResolvedValue(ok({ writable: true, hasDocument, namespaces: [localeView('zh')], scopes: [] }))
+  settings.describe.mockResolvedValue(ok({ writable: true, hasDocument, namespaces: [localeView('zh')] }))
   const c = await start()
   // The locale adopts the Host preference once the describe mirror holds the document.
   await c.ctx.settingsScope.describe().ensure()

+ 3 - 4
packages/client/ui-settings-models/tests/apply.client.spec.ts

@@ -262,14 +262,13 @@ describe('pushed invalidations', () => {
     const mock = RemoteMock.create().load(remoteDefaultResponses)
     const namespace = {
       ns: WELCOME_NOTICE_SETTINGS_NAMESPACE,
-      registered: true,
       schema: {},
       value: {},
       applies: 'live' as const,
       secrets: [],
       revision: 0,
     }
-    const document = { writable: true, hasDocument: false, namespaces: [namespace], scopes: [] }
+    const document = { writable: true, hasDocument: false, namespaces: [namespace] }
     mock.remote.settings.describe.mockResolvedValue(ok(document))
     const b = await bench(true, mock)
     declare(b.slots)
@@ -296,8 +295,8 @@ describe('pushed invalidations', () => {
 
   it('joins the refreshed mirror view on a settings invalidation', async () => {
     const mock = RemoteMock.create().load(remoteDefaultResponses)
-    const namespace = { ns: 'llm-test', registered: true, schema: {}, value: {}, applies: 'live' as const, secrets: [], revision: 1 }
-    const document = { writable: true, hasDocument: false, namespaces: [namespace], scopes: [] }
+    const namespace = { ns: 'llm-test', schema: {}, value: {}, applies: 'live' as const, secrets: [], revision: 1 }
+    const document = { writable: true, hasDocument: false, namespaces: [namespace] }
     const describe = mock.remote.settings.describe
     describe.mockResolvedValue(ok(document))
     const listProviders = vi.fn(() => Promise.resolve({ ok: true as const, value: [] }))

+ 0 - 6
packages/client/ui-settings-models/tests/components.client.spec.tsx

@@ -94,7 +94,6 @@ function wireNamespaces(): SettingsNamespaceView[] {
   return [
     {
       ns: 'llm-deepseek',
-      registered: true,
       schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as JsonValue,
       value: {
         apiKeyEnv: 'DEEPSEEK_API_KEY',
@@ -111,7 +110,6 @@ function wireNamespaces(): SettingsNamespaceView[] {
     },
     {
       ns: 'llm-plain',
-      registered: true,
       schema: JSON.parse(JSON.stringify(Schema.object({
         profiles: Schema.dict(Schema.object({ note: Schema.string() })),
       }).toJSON())) as JsonValue,
@@ -122,7 +120,6 @@ function wireNamespaces(): SettingsNamespaceView[] {
     },
     {
       ns: 'llm-pi-ai',
-      registered: true,
       schema: JSON.parse(JSON.stringify(PiAiConfig.toJSON())) as JsonValue,
       value: { providers: { openai: { apiKeyEnv: 'OPENAI_API_KEY', baseURL: 'https://proxy', headers: { 'X-Team': 'a' } }, zombie: {} } },
       user: { providers: { openai: { apiKeyEnv: 'OPENAI_API_KEY', baseURL: 'https://proxy', headers: { 'X-Team': 'a' } }, zombie: {} } },
@@ -132,7 +129,6 @@ function wireNamespaces(): SettingsNamespaceView[] {
     },
     {
       ns: 'subagent-model-selection',
-      registered: true,
       schema: JSON.parse(JSON.stringify(Schema.object({ enabled: Schema.boolean().default(false) }).toJSON())) as JsonValue,
       value: { enabled: false },
       applies: 'live',
@@ -853,7 +849,6 @@ describe('ModelsSection', () => {
     const stored = { models: [{ id: 'user-only-model', name: 'User Only' }] }
     const overridden: SettingsNamespaceView = {
       ns: 'llm-deepseek',
-      registered: true,
       schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as JsonValue,
       value: { ...stored, defaultContextWindow: 1_000_000 },
       ...base === undefined ? {} : { base },
@@ -1086,7 +1081,6 @@ describe('ModelsSection', () => {
     const { face } = scriptedFace()
     const bare: SettingsNamespaceView = {
       ns: 'llm-deepseek',
-      registered: true,
       schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as JsonValue,
       value: {},
       applies: 'live',

+ 0 - 1
packages/client/ui-settings-models/tests/onboarding-dialog.client.spec.tsx

@@ -53,7 +53,6 @@ function deepSeekNamespace(apiKeyEnv: string | null): SettingsNamespaceView {
   const value = apiKeyEnv === null ? {} : { apiKeyEnv }
   return {
     ns: 'llm-deepseek',
-    registered: true,
     schema: JSON.parse(JSON.stringify(DeepSeekConfig.toJSON())) as JsonValue,
     value,
     base: value,

+ 0 - 1
packages/client/ui-settings-models/tests/provider-form.client.spec.tsx

@@ -80,7 +80,6 @@ function piAiNamespace(
 ): SettingsNamespaceView {
   return {
     ns: 'llm-pi-ai',
-    registered: true,
     schema: JSON.parse(JSON.stringify(PiAiConfig.toJSON())) as JsonValue,
     // `value` is the effective section; `user` is only the layer this page
     // writes. They differ whenever a composition `base` supplies something.

+ 2 - 2
packages/client/ui-settings-plugins/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/client/ui-settings-plugins/README.md
-README.md: 60508bbd2701f151d03ace026da8957c5a35bda4
-README.zh.md: f7f21cba96cff083cf1378a2f1e6e853bbbb211e
+README.md: 99f0cf19d25bb76b34d95b31f8f8fcbf0c596f79
+README.zh.md: 6a32d0e5370fb9b8030013af22abb44449d97784

+ 4 - 8
packages/client/ui-settings-plugins/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-Use the **Plugins** settings section to configure the plugins exposed by the current deployment and to open the pages feature plugins contribute through `settings.plugins.tab`. The **Plugin configuration** tab presents one expandable card for each supported plugin, shows which values the user overrode, and lets the user reset them to deployment defaults; the same cards form the **Settings** section of every preset's detail page, editing that preset's own values and marking which ones it inherits. Cards keep edits local until save. If the configuration changed after the card loaded, the save is rejected instead of overwriting the newer values.
+Use the **Plugins** settings section to configure the plugins exposed by the current deployment and to open feature-specific plugin pages. The **Plugin configuration** tab presents one expandable card for each supported plugin, shows which values the user overrode, and lets the user reset them to deployment defaults. Cards keep edits local until save. If the configuration changed after the card loaded, the save is rejected instead of overwriting the newer values.
 
 ## Table of Contents
 
@@ -25,11 +25,7 @@ Use the **Plugins** settings section to configure the plugins exposed by the cur
 <a id="use-this-package"></a>
 ## Use this package
 
-Open the Plugins section in Settings and select the **Plugin configuration** tab to edit the host-plane plugins this deployment composes. The cards appear in this order: the shell executor (`bash`), the agent loop's tool-call parallelism (`agent-loop`), subagent model selection (`subagent-model-selection`), the DeepSeek search provider (`web-search-deepseek`), and the filesystem skill provider's extra roots (`skill-filesystem`).
-
-### A preset's own values
-
-A preset's detail page — behind the gear on its card in the Agent presets section — carries a **Settings** section with the same cards, editing that preset's named scope (`preset/<id>`). There a field the preset does not override shows **Inherited** when the shared user layer carries it, and a reset stages the inherited value rather than the composition default. Saved values land in the preset's own section: a running session composed from the preset picks them up at once, and later sessions read them when they start. Drafts belong to the scope they were typed under and survive leaving the page. The configuration tab always edits the shared values.
+Open the Plugins section in Settings and select the **Plugin configuration** tab to edit the host-plane plugins this deployment composes. The cards appear in this order: the shell executor (`bash`), the agent loop's tool-call parallelism (`agent-loop`), subagent model selection (`subagent-model-selection`), and the DeepSeek search provider (`web-search-deepseek`).
 
 ### What appears here
 
@@ -57,7 +53,7 @@ The section is one extension point and one dispatch rule: feature plugins own th
 
 ### The tab extension point
 
-The section declares `settings.plugins.tab`, a root list slot whose labels become ordered tabs — a single contribution shows as the page itself, without a tab strip; a tab stays mounted after its first selection so local drafts and read-only snapshots survive tab switches. The package registers its own `configurable` contribution, which declares the nested `settings.plugin.item` slot — keyed on the settings namespace a card edits. A plugin that ships a browser half registers its own card under its own namespace and owns every part of it: chrome, controls, and copy. Tabs follow the contribution's `order`; cards follow registration order. The preset settings section registers into the roster's `settings.agentPreset.detail` slot and declares `settings.agentPreset.plugin.item`, keyed the same way; a card registers under both slots to appear on both surfaces. One scope selection serves every card — the global instance while the configuration tab shows, the preset's scope while its detail page is open; the settings shell mounts one section at a time, so the two never compete.
+The section declares `settings.plugins.tab`, a root list slot whose labels become ordered tabs; a tab stays mounted after its first selection so local drafts and read-only snapshots survive tab switches. The package registers its own `configurable` contribution, which declares the nested `settings.plugin.item` slot — keyed on the settings namespace a card edits. A plugin that ships a browser half registers its own card under its own namespace and owns every part of it: chrome, controls, and copy. Tabs follow the contribution's `order`; cards follow registration order.
 
 ### The write path
 
@@ -73,6 +69,7 @@ Saving writes staged fields through the client settings scope, which fences each
 These pages cover the settings base, the inventory tab, and the durable seams behind the cards.
 
 - [ui-settings](../ui-settings/README.md) — the domain base declaring `settings.plugins.tab` and the settings scope.
+- [ui-plugin-manager](../ui-plugin-manager/README.md) — the sidebar page for installed packages and global rows.
 - [settings](../../settings/README.md) — the durable user-settings seam and its file provider.
 - [credentials](../../credentials/README.md) — the credential-reference seam secret fields write through.
 - [ui-settings-general](../ui-settings-general/README.md) — the settings shell hosting this section.
@@ -98,7 +95,6 @@ These limits define which plugins appear and how fresh the list is; they are cur
 - **Only host-plane plugins appear** — a plugin an agent preset mounts carries its configuration inline in that preset's `agent.cordis.yml` and cannot register a settings namespace at all, so this section lists nothing for it. Editing those values remains the preset editor's job.
 - **A card still needs a browser bundle** — the browser half must be a `dsh.client` package built in the client module system's lazy-CJS factory format, and the `clientBundle` preset that emits it lives in `../../../packages/client/tsdown.client.ts` rather than a published package, so a plugin outside this repository has to reproduce that build itself.
 - **The served namespaces re-read on two signals only** — the wire announces settings-document commits and connection resets, not registrations, so a namespace whose owner registers after the tab's read joins the list on the next document commit or reconnect.
-- **An orphaned scope section has no surface** — a `scopes.<id>` section whose preset was deleted is edited nowhere: the detail page that would show it is gone, and the configuration tab edits only the shared values, so the section stays in the document until edited by hand.
 - **The shell card follows the composed executor** — the POSIX and PowerShell executor families share the `bash` namespace because a host composes exactly one of them, so the served schema differs by platform (PowerShell adds `pwshPath`) even though the card edits the same two fields on both.
 
 <a id="dev-note"></a>

+ 4 - 8
packages/client/ui-settings-plugins/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-使用**插件**设置分区可以配置当前部署公开的插件,也可以打开功能插件经 `settings.plugins.tab` 贡献的页面。**插件配置**标签页会为每个受支持的插件展示一张可展开卡片,标明用户覆盖过哪些值,并允许用户将它们重置为部署默认值;同样的卡片构成每个预设详情页的**设置**段,在那里编辑该预设自己的值并标注哪些是继承来的。卡片会在本地保留修改,直到用户保存。如果配置在卡片加载后发生变化,保存会被拒绝,而不会覆盖较新的值。
+使用**插件**设置分区可以配置当前部署公开的插件,也可以打开插件功能自己的页面。**插件配置**标签页会为每个受支持的插件展示一张可展开卡片,标明用户覆盖过哪些值,并允许用户将它们重置为部署默认值。卡片会在本地保留修改,直到用户保存。如果配置在卡片加载后发生变化,保存会被拒绝,而不会覆盖较新的值。
 
 ## 目录
 
@@ -25,11 +25,7 @@ kind: "package-reference"
 <a id="use-this-package"></a>
 ## 使用本包
 
-打开设置中的「插件」分区并选择**插件配置**标签页,即可编辑本部署所组装的宿主平面插件。卡片依次为 shell 执行器(`bash`)、agent 循环的工具调用并行度(`agent-loop`)、subagent 模型选择(`subagent-model-selection`)、DeepSeek 搜索提供方(`web-search-deepseek`)以及文件系统技能提供方的额外目录(`skill-filesystem`)。
-
-### 预设自己的值
-
-预设的详情页——Agent 预设分区里卡片上的齿轮之后——带一个**设置**段,用同样的卡片编辑该预设的命名作用域(`preset/<id>`)。在那里,预设未覆盖的字段在共用用户层持有它时显示**继承**,重置暂存的是继承值而非组装默认值。保存的值写进该预设自己的段:由该预设组装的运行中会话立即读到,之后的会话在启动时读取。草稿属于输入时所在的作用域,离开页面后仍在。配置标签页始终编辑共用值。
+打开设置中的「插件」分区并选择**插件配置**标签页,即可编辑本部署所组装的宿主平面插件。卡片依次为 shell 执行器(`bash`)、agent loop(智能体循环)的工具调用并行度(`agent-loop`)、subagent 模型选择(`subagent-model-selection`)以及 DeepSeek 搜索提供方(`web-search-deepseek`)。
 
 ### 这里会出现什么
 
@@ -57,7 +53,7 @@ subagent 卡会同时暂存其权限开关与精确模型复选框。启用时
 
 ### 标签页扩展点
 
-本分区声明根级列表 slot `settings.plugins.tab`,其标签会成为有序标签页——只有一个贡献时它直接就是这一页,没有标签条;某个标签页首次被选择后会保持挂载,因此本地草稿与只读快照在切换标签页时不会丢失。本包注册自己的 `configurable` 贡献,由它声明嵌套的 `settings.plugin.item` slot——以卡片所编辑的 settings 命名空间为键。带浏览器半侧的插件把自己的卡片注册在自己的命名空间上,并拥有它的全部:外观、控件与文案。标签页遵循贡献的 `order`;卡片遵循注册顺序。预设设置段注册进名单的 `settings.agentPreset.detail` 槽位并声明按同样方式键控的 `settings.agentPreset.plugin.item`;卡片在两个槽位下都注册才会出现在两个表面上。一个作用域选择服务所有卡片——配置标签页显示时是全局实例,预设详情页打开时是该预设的作用域;设置外壳一次只挂载一个分区,因此两者从不竞争。
+本分区声明根级列表 slot `settings.plugins.tab`,其标签会成为有序标签页;某个标签页首次被选择后会保持挂载,因此本地草稿与只读快照在切换标签页时不会丢失。本包注册自己的 `configurable` 贡献,由它声明嵌套的 `settings.plugin.item` slot——以卡片所编辑的 settings 命名空间为键。带浏览器半侧的插件把自己的卡片注册在自己的命名空间上,并拥有它的全部:外观、控件与文案。标签页遵循贡献的 `order`;卡片遵循注册顺序。
 
 ### 写入路径
 
@@ -73,6 +69,7 @@ subagent 卡会同时暂存其权限开关与精确模型复选框。启用时
 以下页面覆盖设置底座、清单标签页与卡片背后的持久化 seam。
 
 - [ui-settings](../ui-settings/README.zh.md)——声明 `settings.plugins.tab` 与 settings scope 的领域底座。
+- [ui-plugin-manager](../ui-plugin-manager/README.zh.md)——管理已安装包与全局行的侧栏页面。
 - [settings](../../settings/README.zh.md)——持久化用户设置 seam 及其文件提供方。
 - [credentials](../../credentials/README.zh.md)——secret 字段写入所经的凭据引用 seam。
 - [ui-settings-general](../ui-settings-general/README.zh.md)——承载本分区的设置外壳。
@@ -98,7 +95,6 @@ subagent 卡会同时暂存其权限开关与精确模型复选框。启用时
 - **只有宿主平面的插件会出现**:由 agent preset 挂载的插件把配置内联在该 preset 的 `agent.cordis.yml` 中,且根本无法注册 settings 命名空间,因此本分区不会列出它。编辑那些值仍是 preset 编辑器的职责。
 - **卡片仍然需要一份浏览器 bundle**:浏览器半侧必须是按客户端模块系统的 lazy-CJS factory 格式构建的 `dsh.client` 包,而产出它的 `clientBundle` 预设位于 `../../../packages/client/tsdown.client.ts`,并非已发布的包,因此本仓库之外的插件得自行复刻该构建。
 - **被服务的命名空间只在两种信号上重读**:协议通告的是 settings 文档提交与连接重置,而非注册行为,因此在标签页读取之后才被其拥有方注册的命名空间,要等下一次文档提交或重连才会加入列表。
-- **孤儿作用域段没有界面**:预设已被删除的 `scopes.<id>` 段无处编辑——会显示它的详情页已不存在,而配置标签页只编辑共用值,因此该段一直留在文档里,直到手工编辑。
 - **shell 卡片跟随被组装的执行器**:POSIX 与 PowerShell 两个执行器家族共用 `bash` 命名空间,因为一个宿主只组装其中之一,所以被服务的 schema 随平台不同(PowerShell 多出 `pwshPath`),尽管卡片在两者下编辑的都是同样两个字段。
 
 <a id="dev-note"></a>

+ 1 - 4
packages/client/ui-settings-plugins/package.json

@@ -30,8 +30,7 @@
       "inject": [
         "@deepseek-ai/dsh-client-locale",
         "@deepseek-ai/dsh-client-ui-settings",
-        "@deepseek-ai/dsh-api-remotes",
-        "@deepseek-ai/dsh-client-ui-agent-preset"
+        "@deepseek-ai/dsh-api-remotes"
       ],
       "platform": "web"
     }
@@ -50,9 +49,7 @@
     "@deepseek-ai/dsh-client-locale": "workspace:^",
     "@deepseek-ai/dsh-client-store": "workspace:^",
     "@deepseek-ai/dsh-client-test-runtime": "workspace:^",
-    "@deepseek-ai/dsh-client-ui-agent-preset": "workspace:^",
     "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
-    "@deepseek-ai/dsh-client-ui-renderer": "workspace:^",
     "@deepseek-ai/dsh-client-ui-settings": "workspace:^",
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@types/react": "~18.3.1",

+ 0 - 1
packages/client/ui-settings-plugins/src/client/AgentLoopCard.tsx

@@ -34,7 +34,6 @@ export function AgentLoopCard(props: AgentLoopCardProps) {
         label={t('agentLoopMaxParallel')}
         hint={t('agentLoopMaxParallelHint')}
         overriddenLabel={t('overridden')}
-        inheritedLabel={t('inherited')}
         resetLabel={t('reset')}
         invalidLabel={t('invalidNumber')}
         numeric

+ 0 - 2
packages/client/ui-settings-plugins/src/client/BashCard.tsx

@@ -35,7 +35,6 @@ export function BashCard(props: BashCardProps) {
         label={t('bashTimeoutMs')}
         hint={t('bashTimeoutMsHint')}
         overriddenLabel={t('overridden')}
-        inheritedLabel={t('inherited')}
         resetLabel={t('reset')}
         invalidLabel={t('invalidNumber')}
         numeric
@@ -49,7 +48,6 @@ export function BashCard(props: BashCardProps) {
         label={t('bashMaxOutputBytes')}
         hint={t('bashMaxOutputBytesHint')}
         overriddenLabel={t('overridden')}
-        inheritedLabel={t('inherited')}
         resetLabel={t('reset')}
         invalidLabel={t('invalidNumber')}
         numeric

+ 14 - 19
packages/client/ui-settings-plugins/src/client/ConfigurablePluginsTab.tsx

@@ -4,8 +4,7 @@
  * The tab enumerates settings namespaces but never interprets one — a card
  * arrives through `settings.plugin.item` keyed by the namespace it edits, so a
  * plugin that ships a browser half owns its own card and this tab only decides
- * which keys to dispatch. Every card edits the global instance of its
- * namespace here; a preset's own values are edited on the preset's detail page.
+ * which keys to dispatch.
  */
 
 import { Fragment } from 'react'
@@ -22,27 +21,23 @@ export type ConfigurablePluginsTabProps =
   & InjectFace<ConfigurablePluginsTabFace>
 
 /**
- * Render the cards registered by plugins that expose editable settings.
+ * Render cards registered by plugins that expose editable settings.
  * @param props - locale copy, slot rendering, and the namespaces to dispatch.
  * @returns the card list, or the empty line once the Host has answered.
  */
 export function ConfigurablePluginsTab(props: ConfigurablePluginsTabProps) {
   const { t, renderSlot } = props
   const { loaded, namespaces } = props.useConfigurablePlugins(snapshot => snapshot)
-
-  return (
-    <div className={css.configurable}>
-      {namespaces.length > 0
-        ? (
-          <ul className={css.cards}>
-            {namespaces.map(ns => (
-              // One dispatch per namespace, so the list identity is the namespace
-              // rather than a position that shifts as cards arrive.
-              <Fragment key={ns}>{renderSlot('settings.plugin.item', {}, { entryKey: ns })}</Fragment>
-            ))}
-          </ul>
-        )
-        : loaded ? <p className={css.empty}>{t('empty')}</p> : null}
-    </div>
-  )
+  if (namespaces.length > 0) {
+    return (
+      <ul className={css.cards}>
+        {namespaces.map(ns => (
+          // One dispatch per namespace, so the list identity is the namespace
+          // rather than a position that shifts as cards arrive.
+          <Fragment key={ns}>{renderSlot('settings.plugin.item', {}, { entryKey: ns })}</Fragment>
+        ))}
+      </ul>
+    )
+  }
+  return loaded ? <p className={css.empty}>{t('empty')}</p> : null
 }

+ 0 - 65
packages/client/ui-settings-plugins/src/client/PresetSettingsSection.tsx

@@ -1,65 +0,0 @@
-/**
- * The settings section of one agent preset's detail page: the plugin cards
- * registered into `settings.agentPreset.plugin.item`, editing the preset's own
- * scope (`preset/<id>`). Contributed to the roster's `settings.agentPreset.detail`
- * slot. While the section is mounted the card scope selection names the
- * preset; unmounting returns every card to the global instance. The settings
- * shell mounts one section at a time, so this page and the configuration tab
- * never hold the selection together.
- */
-
-import { Fragment, useLayoutEffect } from 'react'
-import type { InjectFace, PropsLocale, PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
-// Type-only: the detail slot's declaration, owned by the roster.
-import type {} from '@deepseek-ai/dsh-client-ui-agent-preset/client'
-import type {} from './slot-contract.ts'
-import type { ConfigurablePluginsTabFace } from './tab-store.ts'
-import css from './PluginsSettingsSection.module.css'
-
-/** Scope id prefix the agent-preset roster names its standing scopes with. */
-export const PRESET_SCOPE_PREFIX = 'preset/'
-
-/** The registration-side face the section injects: the card directory plus the scope selection. */
-export interface PresetSettingsFace extends ConfigurablePluginsTabFace {
-  /** Select the scope every card edits; null selects the global instance. */
-  selectScope: (scope: string | null) => void
-}
-
-/** Props the renderer binds for the section. */
-export type PresetSettingsSectionProps =
-  PropsRuntime<'settings.agentPreset.detail'>
-  & PropsLocale<'settings.plugins'>
-  & PropsRenderSlots<'settings.agentPreset.plugin.item'>
-  & InjectFace<PresetSettingsFace>
-
-/**
- * Render one preset's settings: the cards registered by plugins that expose
- * editable settings, under the preset's scope.
- * @param props - locale copy, slot rendering, the namespaces to dispatch, the preset, and the scope selection.
- * @returns the section, with the empty line once the Host has answered.
- */
-export function PresetSettingsSection(props: PresetSettingsSectionProps) {
-  const { t, renderSlot, presetId, selectScope } = props
-  const { loaded, namespaces } = props.useConfigurablePlugins(snapshot => snapshot)
-  const scope = `${PRESET_SCOPE_PREFIX}${presetId}`
-  // Before paint, so the cards never show the global instance's values first.
-  useLayoutEffect(() => {
-    selectScope(scope)
-    return () => { selectScope(null) }
-  }, [scope, selectScope])
-
-  return (
-    <section className={css.presetSettings} data-settings-scope={scope}>
-      <h3 className={css.presetSettingsTitle}>{t('presetSettingsTitle')}</h3>
-      {namespaces.length > 0
-        ? (
-          <ul className={css.cards}>
-            {namespaces.map(ns => (
-              <Fragment key={ns}>{renderSlot('settings.agentPreset.plugin.item', {}, { entryKey: ns })}</Fragment>
-            ))}
-          </ul>
-        )
-        : loaded ? <p className={css.empty}>{t('empty')}</p> : null}
-    </section>
-  )
-}

+ 0 - 48
packages/client/ui-settings-plugins/src/client/SkillFilesystemCard.tsx

@@ -1,48 +0,0 @@
-/** The filesystem skill provider's card: the extra roots the agent discovers skills in. */
-
-import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
-import { ValueField } from './fields.tsx'
-import { PluginCard } from './PluginCard.tsx'
-import type { SkillFilesystemCardFace } from './skill-filesystem-card-controller.ts'
-import type {} from './slot-contract.ts'
-
-/** Props the renderer binds for the skill roots card. */
-export type SkillFilesystemCardProps =
-  PropsRuntime<'settings.plugin.item'>
-  & PropsLocale<'settings.plugins'>
-  & InjectFace<SkillFilesystemCardFace>
-
-/**
- * Render the skill roots card.
- * @param props - locale copy, the card snapshot, and its form actions.
- * @returns the card.
- */
-export function SkillFilesystemCard(props: SkillFilesystemCardProps) {
-  const { t } = props
-  const state = props.useSkillFilesystemCard(snapshot => snapshot)
-  return (
-    <PluginCard
-      t={t}
-      titleKey="skillFilesystemTitle"
-      descriptionKey="skillFilesystemDescription"
-      state={state}
-      onSave={props.save}
-      onDiscard={props.discard}
-    >
-      <ValueField
-        id="plugin-config-skill-filesystem-dirs"
-        label={t('skillFilesystemCustomSkillDirs')}
-        hint={t('skillFilesystemCustomSkillDirsHint')}
-        overriddenLabel={t('overridden')}
-        inheritedLabel={t('inherited')}
-        resetLabel={t('reset')}
-        invalidLabel={t('invalidNumber')}
-        multiline
-        disabled={!state.writable}
-        {...state.customSkillDirs}
-        onEdit={(text) => { props.edit('customSkillDirs', text) }}
-        onReset={() => { props.resetField('customSkillDirs') }}
-      />
-    </PluginCard>
-  )
-}

+ 0 - 2
packages/client/ui-settings-plugins/src/client/WebSearchCard.tsx

@@ -53,7 +53,6 @@ export function WebSearchCard(props: WebSearchCardProps) {
         label={t('webSearchBaseUrl')}
         hint={t('webSearchBaseUrlHint')}
         overriddenLabel={t('overridden')}
-        inheritedLabel={t('inherited')}
         resetLabel={t('reset')}
         invalidLabel={t('invalidNumber')}
         disabled={disabled}
@@ -66,7 +65,6 @@ export function WebSearchCard(props: WebSearchCardProps) {
         label={t('webSearchMaxUses')}
         hint={t('webSearchMaxUsesHint')}
         overriddenLabel={t('overridden')}
-        inheritedLabel={t('inherited')}
         resetLabel={t('reset')}
         invalidLabel={t('invalidNumber')}
         numeric

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott