docs(web): follow up the review on the companion move and detail slots

The maintained docs now name the four companion packages where they
still credited `ui-settings-plugins`: the web-app row comment, the
Plugins page README, the ui-settings README, and the client group
README, which gains a row per companion. The ui-settings README states
the `whileServed` lifetime and disposer ownership beside `bind`, and the
JSDoc says the caller owns the disposer. The settings-page cookbook
keeps the one home of the lazy-CJS build limitation for packages outside
this repository, its JSX example is a tsx fence, and the architecture
index and website label follow the page's title. The Agent Note records
the shell page's `plugins.item` id, the two companions that use
`ctx.remote` list api-remotes in `dsh.client.inject`, and the moved form
model test speaks of the form.
This commit is contained in:
Yichen Jiang
2026-09-17 20:13:26 +08:00
parent 93e67201dd
commit caa0c99be1
24 changed files with 62 additions and 33 deletions
@@ -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-17-settings-pages-as-companion-packages.md
2026-09-17-settings-pages-as-companion-packages.md: dc604c010787ee35522b351b643fb2691540b4f9
2026-09-17-settings-pages-as-companion-packages.zh.md: 066b17c9760f381089a6a48b1ec6606e600531d3
2026-09-17-settings-pages-as-companion-packages.md: de2f2897938fcc158c49bed772f399b4d9328d19
2026-09-17-settings-pages-as-companion-packages.zh.md: 27d00faa156f0f48c6dc3995930be9ca629a732d
@@ -33,6 +33,7 @@ The four official settings pages — shell, agent loop, Subagent, web search —
- The Plugins page's Official group lists the same four pages in the same order, from four packages instead of one; their labels and forms are unchanged, so the page goldens hold.
- The cookbook [adding a settings page](../../../../docs/cookbook/adding-a-settings-card.md) shows the community path and names the companion template.
- The client module system's rule stands: a browser half rides the Loader row whose specifier is the bare package name, so a bundle split into subpath rows keeps every page it registers on its root row.
- The shell page's `plugins.item` id is `shell`, where the old package registered `bash`: `subject.id` on an official page is now a discriminator `plugins.detail.*` contributions key on, so the id names the capability the page edits rather than one executor family.
## Testing
@@ -33,6 +33,7 @@
- 插件页官方分组列出同样四个页面、同样顺序,来自四个包而不是一个;标签与表单未变,页面 golden 保持不变。
- 手册[添加设置页](../../../../docs/cookbook/adding-a-settings-card.zh.md)展示社区路径并点名伴生包模板。
- 客户端模块系统的规则不变:浏览器半侧挂在说明符为裸包名的 Loader 行上,因此拆成子路径行的组合包,其注册的所有页面都留在根行上。
- 终端页在 `plugins.item` 里的 id 是 `shell`,旧包注册的是 `bash`:官方页面上的 `subject.id` 现在是 `plugins.detail.*` 贡献可以据以判断的标识,所以 id 以页面编辑的能力命名,而不是某一个执行器家族。
## 测试
+2 -2
View File
@@ -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/architecture.md
architecture.md: 37aaf83e37fb5cb7e3df4bd6ed034ccd42103919
architecture.zh.md: 084fa76a045a9ae744228e8e43900f174b26ae9e
architecture.md: ebb28a79fb17ff1364b59dddfaeb5b6eb723bd1f
architecture.zh.md: 0a19671c6bb36df689585e8f5654ce721834bd49
+1 -1
View File
@@ -161,4 +161,4 @@ New behavior attaches to a documented extension point. Changing the loop itself
| Store sessions in a new backend | implement `SessionPersistence` (`create`/`open`/`stat`/`list`/`export`) over the shared handle scaffolding |
| Scope a registration to one agent | use that agent's `agent.ctx` |
The [extension cookbook](cookbook/extension-cookbook.md) maps features to capabilities and indexes the step-by-step guides for [packages](cookbook/adding-a-package.md), [tools](cookbook/adding-a-tool.md), [LLM adapters](cookbook/adding-an-llm-adapter.md), and [settings cards](cookbook/adding-a-settings-card.md). The [Conversation subsystem](subsystems/conversation.md) owns Chat-node assembly.
The [extension cookbook](cookbook/extension-cookbook.md) maps features to capabilities and indexes the step-by-step guides for [packages](cookbook/adding-a-package.md), [tools](cookbook/adding-a-tool.md), [LLM adapters](cookbook/adding-an-llm-adapter.md), and [settings pages](cookbook/adding-a-settings-card.md). The [Conversation subsystem](subsystems/conversation.md) owns Chat-node assembly.
+1 -1
View File
@@ -165,4 +165,4 @@ seam 正是替换一个提供方就能改变整个产品的原因。文件系统
| 在新后端存储会话 | 基于共享的句柄脚手架实现 `SessionPersistence`(`create`/`open`/`stat`/`list`/`export`) |
| 将注册项限定到单个 agent | 使用该 agent 的 `agent.ctx` |
[扩展实操手册](cookbook/extension-cookbook.zh.md)将功能映射到能力,并索引[包](cookbook/adding-a-package.zh.md)、[工具](cookbook/adding-a-tool.zh.md)、[LLM(大语言模型)适配器](cookbook/adding-an-llm-adapter.zh.md)和[设置卡片](cookbook/adding-a-settings-card.zh.md)的分步指南。[Conversation 子系统](subsystems/conversation.zh.md)负责 Chat node 组装。
[扩展实操手册](cookbook/extension-cookbook.zh.md)将功能映射到能力,并索引[包](cookbook/adding-a-package.zh.md)、[工具](cookbook/adding-a-tool.zh.md)、[LLM(大语言模型)适配器](cookbook/adding-an-llm-adapter.zh.md)和[设置页](cookbook/adding-a-settings-card.zh.md)的分步指南。[Conversation 子系统](subsystems/conversation.zh.md)负责 Chat node 组装。
@@ -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/cookbook/adding-a-settings-card.md
adding-a-settings-card.md: cc8daa90bcc3a757cd41dbcaecb0eab662d1dba8
adding-a-settings-card.zh.md: a0fce4075b9e29630c772bc52e07cd00d89d5bab
adding-a-settings-card.md: 72d7eaeb85d0d4f6981e814a8c335428c561b6bd
adding-a-settings-card.zh.md: d1254078085ba1de541ea70db566bcfb1bd50d1d
+3 -1
View File
@@ -84,7 +84,7 @@ export function apply(ctx: ClientContext): void {
A plugin with something to say about a bundle, a row, or an official plugin it does not own registers into `plugins.detail.actions` (a control at the head of the page), `plugins.detail.badge` (a tag beside the title), or `plugins.detail.section` (a section under the page's own content). Every entry is rendered with the page's `subject` — `{ kind: 'bundle', pkg }`, `{ kind: 'row', pkg, row }`, or `{ kind: 'item', id }` — and returns null for a subject it has nothing for:
```ts ignore-check
```tsx ignore-check
ctx.slots.inject('plugins.detail.badge', () => ctx.slots.register({
name: 'plugins.detail.badge',
id: 'acme-update',
@@ -96,6 +96,8 @@ ctx.slots.inject('plugins.detail.badge', () => ctx.slots.register({
The browser half is served to the page by the [client module system](../../packages/client/modules), which scans the enabled Loader entries for packages declaring `dsh.client` and serves each one's built `./client` export — but it attaches a package's half to the Loader row whose specifier is the bare package name. A row mounted from a subpath export never carries a half, so a bundle that splits one package into several rows keeps its half on the root row, and every page it registers goes away when that row is switched off. A sub-plugin whose page must outlive the other rows ships as its own package.
The built `./client` file must be in the client module system's lazy-CJS factory format: one script that registers the package name and a `factory(require)` with the page's module loader, described in the [client module system's README](../../packages/client/modules/README.md). The `clientBundle` tsdown preset that emits it lives in `packages/client/tsdown.client.ts` rather than in a published package, so a package outside this repository reproduces that build itself.
```jsonc
{
"exports": {
+3 -1
View File
@@ -84,7 +84,7 @@ export function apply(ctx: ClientContext): void {
对某个不属于自己的组合包、行或官方插件有话要说的插件,注册进 `plugins.detail.actions`(页头的控件)、`plugins.detail.badge`(标题旁的标签)或 `plugins.detail.section`(页面自身内容之下的区块)。每个条目都以页面的 `subject` 渲染——`{ kind: 'bundle', pkg }`、`{ kind: 'row', pkg, row }` 或 `{ kind: 'item', id }`——对无话可说的 subject 返回 null:
```ts ignore-check
```tsx ignore-check
ctx.slots.inject('plugins.detail.badge', () => ctx.slots.register({
name: 'plugins.detail.badge',
id: 'acme-update',
@@ -96,6 +96,8 @@ ctx.slots.inject('plugins.detail.badge', () => ctx.slots.register({
浏览器半侧由[客户端模块系统](../../packages/client/modules)送到页面:它扫描已启用的 Loader 条目,找出声明了 `dsh.client` 的包,送出每个包构建好的 `./client` 导出——但它只把一个包的半侧挂在说明符恰为裸包名的那一行上。从子路径导出挂载的行永远不带半侧,因此把一个包拆成多行的组合包,其半侧留在根行上,它注册的每个页面都随根行关闭而消失。需要在其他行关闭时仍保留页面的子插件,应作为独立的包发布。
构建出的 `./client` 文件必须是客户端模块系统的 lazy-CJS factory 格式:一段脚本,向页面的模块加载器登记包名和一个 `factory(require)`,见[客户端模块系统的 README](../../packages/client/modules/README.zh.md)。生成它的 `clientBundle` tsdown 预设位于 `packages/client/tsdown.client.ts`,而不在任何已发布的包里,因此仓库之外的包要自己复刻这一步构建。
```jsonc
{
"exports": {
+3 -2
View File
@@ -359,8 +359,9 @@
- id: ui-agent-preset
name: '@deepseek-ai/dsh-client-ui-agent-preset'
# Plugin configuration: the host-plane sections a user owns, as expandable
# cards. A namespace this deployment does not expose renders nothing.
# The Built-in plugins settings section: the navigation entry and the tab
# row the inventory tab registers into. The official configuration pages
# are the companion rows below.
- id: ui-settings-plugins
name: '@deepseek-ai/dsh-client-ui-settings-plugins'
+2 -2
View File
@@ -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/README.md
README.md: 3a1a54f801090456a1038d0e3d59fe5223374f3c
README.zh.md: 231449ab574224a3e4fc5436c57e9ece5b0183b5
README.md: 7b6c88ebbbfdf97f1ec191a2e8133c488ecb422d
README.zh.md: 11846c189afd85817d33e8266bb105b5f5418314
+5 -1
View File
@@ -67,12 +67,16 @@ The kernel packages boot and serve the page; the UI feature packages present it.
| [`ui-model-selection/`](ui-model-selection/README.md) | Provides model selection in conversation surfaces | — |
| [`ui-permission-presets/`](ui-permission-presets/README.md) | Configures default permissions and switches the current session's access | — |
| [`ui-plan/`](ui-plan/README.md) | Presents active plan-mode status and its exit control | — |
| [`ui-settings-plugins/`](ui-settings-plugins/README.md) | Owns the Plugins settings section, its tab extension point, and configurable host-plane plugin cards | — |
| [`ui-settings-plugins/`](ui-settings-plugins/README.md) | Owns the Built-in plugins settings section shell and its tab extension point | — |
| [`ui-user-questions/`](ui-user-questions/README.md) | Presents interactive questions requested by the agent | — |
| [`ui-agent-preset/`](ui-agent-preset/README.md) | Selects a session's agent preset and authors preset compositions | — |
| [`ui-settings/`](ui-settings/README.md) | Hosts the settings interface and its extension areas | — |
| [`ui-settings-general/`](ui-settings-general/README.md) | Provides the general settings section | — |
| [`ui-settings-models/`](ui-settings-models/README.md) | Provides model-provider configuration and DeepSeek onboarding | — |
| [`ui-settings-shell/`](ui-settings-shell/README.md) | Provides the shell settings page on the Plugins page | — |
| [`ui-settings-agent-loop/`](ui-settings-agent-loop/README.md) | Provides the agent-loop settings page on the Plugins page | — |
| [`ui-settings-subagent/`](ui-settings-subagent/README.md) | Provides the Subagent settings page on the Plugins page | — |
| [`ui-settings-web-search/`](ui-settings-web-search/README.md) | Provides the web-search settings page on the Plugins page | — |
| [`ui-plugin-manager/`](ui-plugin-manager/README.md) | Contributes the sidebar Plugins panel: install, enable, disable, retry, and compose installed packages | — |
| [`ui-settings-plugin-inventory/`](ui-settings-plugin-inventory/README.md) | Contributes the read-only Host Loader inventory tab to Plugins settings | — |
| [`ui-deliverables/`](ui-deliverables/README.md) | Produces the changed-files card with its comparison tabs, delivery cards, and clickable final-response file references | — |
+5 -1
View File
@@ -67,12 +67,16 @@ kind: "package-group"
| [`ui-model-selection/`](ui-model-selection/README.zh.md) | 在对话界面中提供模型选择 | — |
| [`ui-permission-presets/`](ui-permission-presets/README.zh.md) | 配置默认权限并切换当前会话的访问模式 | — |
| [`ui-plan/`](ui-plan/README.zh.md) | 展示生效中的 plan mode 状态及其退出控件 | — |
| [`ui-settings-plugins/`](ui-settings-plugins/README.zh.md) | 负责「插件」设置分区、其标签页扩展点与可配置的宿主平面插件卡片 | — |
| [`ui-settings-plugins/`](ui-settings-plugins/README.zh.md) | 负责「内置插件」设置分区壳及其标签页扩展点 | — |
| [`ui-user-questions/`](ui-user-questions/README.zh.md) | 展示 agent 请求的交互式问题 | — |
| [`ui-agent-preset/`](ui-agent-preset/README.zh.md) | 选择会话的 agent 预设并编写预设组合 | — |
| [`ui-settings/`](ui-settings/README.zh.md) | 承载设置界面及其扩展区域 | — |
| [`ui-settings-general/`](ui-settings-general/README.zh.md) | 提供常规设置分区 | — |
| [`ui-settings-models/`](ui-settings-models/README.zh.md) | 提供模型提供方配置与 DeepSeek 引导 | — |
| [`ui-settings-shell/`](ui-settings-shell/README.zh.md) | 在插件页提供终端设置页 | — |
| [`ui-settings-agent-loop/`](ui-settings-agent-loop/README.zh.md) | 在插件页提供 Agent 循环设置页 | — |
| [`ui-settings-subagent/`](ui-settings-subagent/README.zh.md) | 在插件页提供 Subagent 设置页 | — |
| [`ui-settings-web-search/`](ui-settings-web-search/README.zh.md) | 在插件页提供网页搜索设置页 | — |
| [`ui-plugin-manager/`](ui-plugin-manager/README.zh.md) | 贡献侧栏的“插件”面板:安装、启用、停用、重试与组合已安装的包 | — |
| [`ui-settings-plugin-inventory/`](ui-settings-plugin-inventory/README.zh.md) | 向「插件」设置贡献只读的 Host Loader 清单标签页 | — |
| [`ui-deliverables/`](ui-deliverables/README.zh.md) | 生成改动文件卡片及其对比 tab、交付文件卡片与可点击的最终响应文件引用 | — |
@@ -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: 4b6c6752f8dc71f7c00324cf56d236631810f752
README.zh.md: f7a9ebe9efba83e6b9f5fb46b6422c2e099ead04
README.md: a31960cab79b693f3d0f3b01a26d484b96ec404f
README.zh.md: 86f06fbeaaf0935f4d4cbdc639e95e6c75173dd6
+2 -2
View File
@@ -45,7 +45,7 @@ A row's switch on the bundle's page calls `pluginManager.setPluginEnabled`, whic
### Configuration pages
A plugin that carries its own configuration renders it on this page rather than in Settings, through three slots the page declares: `plugins.item` (list) for an official plugin, listed in the Official group by its `label`; `plugins.bundle.config` (keyed by the bundle's package name) for a bundle's own configuration, shown on the bundle's page between its description and its rows; and `plugins.row.config` (keyed by `<package name>#<row id>`) for one row's configuration, which gives that row a **Configure** control opening the row's page. The page asks every entry for two views through its owner props: `view: 'summary'` for the one-liner under the title, `view: 'page'` for the form with its own save control. Only a save writes: the page draws the title, icon, and crumb, and the entry's form drops its staged edits when the page is left. The four host-plane pages the installation ships — the shell executor, the agent loop, subagent model selection, and the DeepSeek search provider — come from [ui-settings-plugins](../ui-settings-plugins/README.md), registered while the Host serves their namespaces. A bundle's browser half registers the same way:
A plugin that carries its own configuration renders it on this page rather than in Settings, through three slots the page declares: `plugins.item` (list) for an official plugin, listed in the Official group by its `label`; `plugins.bundle.config` (keyed by the bundle's package name) for a bundle's own configuration, shown on the bundle's page between its description and its rows; and `plugins.row.config` (keyed by `<package name>#<row id>`) for one row's configuration, which gives that row a **Configure** control opening the row's page. The page asks every entry for two views through its owner props: `view: 'summary'` for the one-liner under the title, `view: 'page'` for the form with its own save control. Only a save writes: the page draws the title, icon, and crumb, and the entry's form drops its staged edits when the page is left. The four host-plane pages the installation ships — the shell executor, the agent loop, Subagent, and the DeepSeek search provider — come from one companion package each, [ui-settings-shell](../ui-settings-shell/README.md), [ui-settings-agent-loop](../ui-settings-agent-loop/README.md), [ui-settings-subagent](../ui-settings-subagent/README.md), and [ui-settings-web-search](../ui-settings-web-search/README.md), registered while the Host serves their namespaces. A bundle's browser half registers the same way:
```tsx ignore-check
ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
@@ -105,7 +105,7 @@ These pages cover the sidebar, the Remote calls, and the Host-side manager.
- [ui-sidebar](../ui-sidebar/README.md) — the panel list the Plugins entry registers into; [ui-layout](../ui-layout/README.md) — the main slot the page occupies.
- [api-remotes](../../api/remotes/README.md) — the Remote BFF surface behind `pluginManager.*` and `pluginInventory.*`.
- [plugin-manager](../../boot/plugin-manager/README.md) — the Host-side manager this page drives.
- [ui-settings-plugins](../ui-settings-plugins/README.md) — the official configuration pages that register into this page's slots.
- [ui-settings-shell](../ui-settings-shell/README.md), [ui-settings-agent-loop](../ui-settings-agent-loop/README.md), [ui-settings-subagent](../ui-settings-subagent/README.md), [ui-settings-web-search](../ui-settings-web-search/README.md) — the official configuration pages that register into this page's `plugins.item` slot.
-----
@@ -45,7 +45,7 @@ Agent Teams、Agent Teams Web UI 和 Auto Authorization Review 三个包使用
### 配置页
自带配置的插件把配置渲染在本页而不是设置里,通过本页声明的三个 slot:`plugins.item`(list)用于官方插件,按其 `label` 列在官方分组里;`plugins.bundle.config`(以组合包的包名为键)用于组合包自己的配置,显示在组合包页面的描述与行之间;`plugins.row.config`(以 `<包名>#<行 id>` 为键)用于某一行的配置,这一行由此多出一个**配置**控件,打开该行自己的页面。页面通过 owner props 向每个条目索取两种视图:`view: 'summary'` 是标题下的一句话简介,`view: 'page'` 是带自己保存控件的表单。只有保存才写入:页面负责画标题、图标与面包屑,条目的表单在离开页面时丢弃暂存的修改。安装随附的四个宿主平面配置页——shell 执行器、agent loop、subagent 模型选择、DeepSeek 搜索提供方——来自 [ui-settings-plugins](../ui-settings-plugins/README.zh.md),在 Host 服务其命名空间期间注册。组合包的浏览器半侧用同样的方式注册:
自带配置的插件把配置渲染在本页而不是设置里,通过本页声明的三个 slot:`plugins.item`(list)用于官方插件,按其 `label` 列在官方分组里;`plugins.bundle.config`(以组合包的包名为键)用于组合包自己的配置,显示在组合包页面的描述与行之间;`plugins.row.config`(以 `<包名>#<行 id>` 为键)用于某一行的配置,这一行由此多出一个**配置**控件,打开该行自己的页面。页面通过 owner props 向每个条目索取两种视图:`view: 'summary'` 是标题下的一句话简介,`view: 'page'` 是带自己保存控件的表单。只有保存才写入:页面负责画标题、图标与面包屑,条目的表单在离开页面时丢弃暂存的修改。安装随附的四个宿主平面配置页——shell 执行器、agent loop、Subagent、DeepSeek 搜索提供方——各来自一个伴生包:[ui-settings-shell](../ui-settings-shell/README.zh.md)、[ui-settings-agent-loop](../ui-settings-agent-loop/README.zh.md)、[ui-settings-subagent](../ui-settings-subagent/README.zh.md) 与 [ui-settings-web-search](../ui-settings-web-search/README.zh.md),在 Host 服务其命名空间期间注册。组合包的浏览器半侧用同样的方式注册:
```tsx ignore-check
ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
@@ -105,7 +105,7 @@ ctx.slots.inject('plugins.detail.section', () => ctx.slots.register({
- [ui-sidebar](../ui-sidebar/README.zh.md)——插件入口注册进的面板列表;[ui-layout](../ui-layout/README.zh.md)——页面占用的主 slot。
- [api-remotes](../../api/remotes/README.zh.md)——`pluginManager.*` 与 `pluginInventory.*` 背后的 Remote BFF 面。
- [plugin-manager](../../boot/plugin-manager/README.zh.md)——本页驱动的 Host 侧管理器。
- [ui-settings-plugins](../ui-settings-plugins/README.zh.md)——注册进本页 slot 的官方配置页。
- [ui-settings-shell](../ui-settings-shell/README.zh.md)、[ui-settings-agent-loop](../ui-settings-agent-loop/README.zh.md)、[ui-settings-subagent](../ui-settings-subagent/README.zh.md)、[ui-settings-web-search](../ui-settings-web-search/README.zh.md)——注册进本页 `plugins.item` slot 的官方配置页。
-----
@@ -270,7 +270,7 @@ describe('SettingsFormModel', () => {
expect(store.getSnapshot()).toBe('2000')
})
it('refuses to address a field the card never declared', () => {
it('refuses to address a field the form never declared', () => {
const { subject } = form()
expect(() => subject.field('nope')).toThrow('settings form has no field nope')
@@ -30,7 +30,8 @@
"inject": [
"@deepseek-ai/dsh-client-locale",
"@deepseek-ai/dsh-client-ui-settings",
"@deepseek-ai/dsh-client-ui-plugin-manager"
"@deepseek-ai/dsh-client-ui-plugin-manager",
"@deepseek-ai/dsh-api-remotes"
],
"platform": "web"
}
@@ -30,7 +30,8 @@
"inject": [
"@deepseek-ai/dsh-client-locale",
"@deepseek-ai/dsh-client-ui-settings",
"@deepseek-ai/dsh-client-ui-plugin-manager"
"@deepseek-ai/dsh-client-ui-plugin-manager",
"@deepseek-ai/dsh-api-remotes"
],
"platform": "web"
}
+2 -2
View File
@@ -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/README.md
README.md: 760b4a4567481b561ce319926149815119018177
README.zh.md: f8e2128d734b650b7cd7a10c180f90ea01b77ab5
README.md: d19de7bea23b1d752b977aea06d868e029fc3331
README.zh.md: 5e30e94f7436dc93c3d6f06a58ed771aa314bff7
+7 -2
View File
@@ -31,6 +31,10 @@ Feature plugins use this package to store and edit their preferences without re-
A feature calls `ctx.settingsScope.bind(spec)` with a per-namespace spec and gets a scope derived from the shared document mirror. The scope snapshot carries the resolved section, composition `base`, raw `user`, revision, writability, and host/memory mode; a field is overridden when it is present in `user`, even when its value equals `base`, and `unset` clears that override. Writes go through the scope: `set` and `unset` submit one operation, while `mutate` submits several ordered operations atomically. Each write is fenced by the namespace revision as `expectedRevision`, so a concurrent write from another surface is refused instead of silently overwritten. A staged editor can supply the revision where its draft began as a fixed fence; otherwise the scope uses the latest queued or mirrored revision.
### Following served namespaces
A page that edits a namespace another plugin owns registers through `ctx.settingsScope.whileServed(namespaces, register)`: `register` runs once any listed namespace is in the shared mirror and returns the registration's disposer, which runs when none of them is served any more or when the disposer `whileServed` returns runs. The caller owns that returned disposer and wraps it in `ctx.effect`; unlike `bind`, the service registers nothing on the caller's context. A deployment that never composed the owner therefore shows no trace of the page, and a namespace the Host stops serving withdraws it. The four official pages on the Plugins page ride it, one companion package each.
### Filling the settings slots
A settings surface registers into the slot types this package declares. The shell (`sidebar.settings` occupant, navigation, chrome) lives in ui-settings-general; feature pages register `settings.section` contributions; the Plugins section hosts `settings.plugins.tab` pages; onboarding steps register `settings.onboarding`. Cross-namespace surfaces (schema introspection, the served-namespace directory, `hasDocument`) read the same mirror through `ctx.settingsScope.describe()`.
@@ -55,7 +59,7 @@ The plugin injects `remote` with its `settings` namespace, resolves Host persist
### Scope derivation
`ctx.settingsScope.bind(spec)` returns a per-namespace scope derived from the mirror on the caller's context: the scope's disposer belongs to the calling fiber, binding adds no wire read, and a row's activation never blocks on the settings transport. Writes stay per-scope: `set` and `unset` are single-operation forms of `mutate`, which copies and queues several ordered field operations behind one namespace revision as `expectedRevision`. A committed mutation folds its answer in, a rejected or failed latest mutation triggers one recovery read, and a superseded one leaves recovery to its successor. The cold-boot read count is pinned by `../../../apps/web/tests/startup-rpc-budget.e2e.ts`; a new direct `settings.describe` caller in client code is a regression against it.
`ctx.settingsScope.bind(spec)` returns a per-namespace scope derived from the mirror on the caller's context: the scope's disposer belongs to the calling fiber, binding adds no wire read, and a row's activation never blocks on the settings transport. Writes stay per-scope: `set` and `unset` are single-operation forms of `mutate`, which copies and queues several ordered field operations behind one namespace revision as `expectedRevision`. A committed mutation folds its answer in, a rejected or failed latest mutation triggers one recovery read, and a superseded one leaves recovery to its successor. The cold-boot read count is pinned by `../../../apps/web/tests/startup-rpc-budget.e2e.ts`; a new direct `settings.describe` caller in client code is a regression against it. `whileServed` subscribes to the same mirror and re-evaluates its served set on every snapshot, so a registration toggles with the describe view and needs no wire read beyond the mirror's `ensure`.
### Schema service
@@ -71,7 +75,8 @@ The plugin injects `remote` with its `settings` namespace, resolves Host persist
These pages cover the settings surface family and the durable seam behind it.
- [ui-settings-general](../ui-settings-general/README.md) — the settings shell: trigger chrome, navigation, General section, onboarding projection.
- [ui-settings-plugins](../ui-settings-plugins/README.md) — the Plugins section and its configurable host-plane cards.
- [ui-settings-plugins](../ui-settings-plugins/README.md) — the Built-in plugins section shell around the inventory tab.
- [ui-settings-shell](../ui-settings-shell/README.md), [ui-settings-agent-loop](../ui-settings-agent-loop/README.md), [ui-settings-subagent](../ui-settings-subagent/README.md), [ui-settings-web-search](../ui-settings-web-search/README.md) — the official configuration pages on the Plugins page, each following its namespaces through `whileServed`.
- [ui-settings-models](../ui-settings-models/README.md) — the Models page and DeepSeek onboarding over this base.
- [settings](../../settings/README.md) — the durable user-settings seam and its file provider.
- [ui-sidebar](../ui-sidebar/README.md) — the sidebar shell whose bottom seat hosts the settings trigger.
+7 -2
View File
@@ -31,6 +31,10 @@ kind: "package-reference"
功能调用 `ctx.settingsScope.bind(spec)` 并传入按命名空间的 spec,得到一个由共享文档镜像派生的 scope。scope 快照携带解析后的分区、组合 `base`、原始 `user`、revision、可写性以及 host/内存模式;字段只要出现在 `user` 中即视为覆盖,即使其值与 `base` 相等,`unset` 会清除该覆盖。写入经 scope 进行:`set` 与 `unset` 提交一个操作,`mutate` 则原子提交多个有序操作。每次写入都以命名空间 revision 作为 `expectedRevision` 围栏,因此来自另一界面的并发写入会被拒绝,而不是被静默覆盖。暂存编辑器可以把开始草拟时读取的 revision 作为固定围栏传入;否则 scope 使用最新排队或镜像 revision。
### 跟随被服务的命名空间
编辑另一个插件所拥有命名空间的页面通过 `ctx.settingsScope.whileServed(namespaces, register)` 注册:只要所列命名空间中的任一个进入共享镜像,`register` 就运行一次并返回该注册的 disposer;当它们全都不再被服务,或 `whileServed` 返回的 disposer 被调用时,这个 disposer 就运行。调用方持有返回的 disposer 并把它包进 `ctx.effect`;与 `bind` 不同,服务不会在调用方的 context 上注册任何东西。因此从未组合过所有者的部署看不到该页面的任何痕迹,Host 停止服务的命名空间会撤下它。插件页上的四个官方页面各自通过一个伴生包走这条路径。
### 填充设置 slot
设置界面会注册进本包声明的 slot 类型。外壳(`sidebar.settings` 占位方、导航、界面框架)位于 ui-settings-general;功能页面注册 `settings.section` 贡献;「插件」分区承载 `settings.plugins.tab` 页面;首次使用引导步骤注册 `settings.onboarding`。跨命名空间的表面(schema 内省、已服务命名空间目录、`hasDocument`)通过 `ctx.settingsScope.describe()` 读同一面镜像。
@@ -55,7 +59,7 @@ kind: "package-reference"
### Scope 派生
`ctx.settingsScope.bind(spec)` 在调用方的 context 上返回一个由镜像派生的按命名空间 scope:scope 的 disposer 归调用方 fiber 所有,绑定不新增任何线路读取,某一行的激活绝不会阻塞在设置传输层上。写入仍归各 scope:`set` 与 `unset` 是 `mutate` 的单操作形式,后者会复制操作列表,并把多个有序字段操作排在同一个作为 `expectedRevision` 的命名空间 revision 之后。提交成功的 mutation 把应答折回镜像,被拒绝或失败的最新 mutation 触发一次恢复读取,被取代的 mutation 把恢复留给后继者。冷启动读取次数由 `../../../apps/web/tests/startup-rpc-budget.e2e.ts` 钉住;客户端代码中新增直连 `settings.describe` 调用即是对它的回归。
`ctx.settingsScope.bind(spec)` 在调用方的 context 上返回一个由镜像派生的按命名空间 scope:scope 的 disposer 归调用方 fiber 所有,绑定不新增任何线路读取,某一行的激活绝不会阻塞在设置传输层上。写入仍归各 scope:`set` 与 `unset` 是 `mutate` 的单操作形式,后者会复制操作列表,并把多个有序字段操作排在同一个作为 `expectedRevision` 的命名空间 revision 之后。提交成功的 mutation 把应答折回镜像,被拒绝或失败的最新 mutation 触发一次恢复读取,被取代的 mutation 把恢复留给后继者。冷启动读取次数由 `../../../apps/web/tests/startup-rpc-budget.e2e.ts` 钉住;客户端代码中新增直连 `settings.describe` 调用即是对它的回归。`whileServed` 订阅同一面镜像,在每次快照变化时重新判断被服务集合,因此注册随 describe 视图开关,除镜像的 `ensure` 外不需要任何线路读取。
### Schema 服务
@@ -71,7 +75,8 @@ kind: "package-reference"
以下页面覆盖设置界面家族及其背后的持久化 seam。
- [ui-settings-general](../ui-settings-general/README.zh.md)——设置外壳:触发控件、导航、「通用」分区、引导投影。
- [ui-settings-plugins](../ui-settings-plugins/README.zh.md)——「插件」分区及其可配置宿主平面卡片。
- [ui-settings-plugins](../ui-settings-plugins/README.zh.md)——围绕清单标签页的「内置插件」分区壳。
- [ui-settings-shell](../ui-settings-shell/README.zh.md)、[ui-settings-agent-loop](../ui-settings-agent-loop/README.zh.md)、[ui-settings-subagent](../ui-settings-subagent/README.zh.md)、[ui-settings-web-search](../ui-settings-web-search/README.zh.md)——插件页上的官方配置页,各自通过 `whileServed` 跟随其命名空间。
- [ui-settings-models](../ui-settings-models/README.zh.md)——建立在本底座之上的 Models 页面与 DeepSeek 引导。
- [settings](../../settings/README.zh.md)——持久化用户设置 seam 及其文件提供方。
- [ui-sidebar](../ui-sidebar/README.zh.md)——底部席位承载设置触发控件的侧边栏外壳。
@@ -303,7 +303,9 @@ export class SettingsScopeBinder extends Service {
* disposer runs when none is or when the returned disposer runs. A plugin
* whose page edits a namespace another plugin owns registers the page
* through this, so a deployment that never composed the owner shows no
* trace of the page.
* trace of the page. The caller owns the returned disposer and wraps it in
* `ctx.effect`; unlike {@link bind}, nothing is registered on the caller's
* context here.
* @param namespaces - the settings namespaces the registration follows.
* @param register - registers the contribution; returns its disposer.
* @returns the disposer ending the watch and any live registration.
+1 -1
View File
@@ -440,7 +440,7 @@ const reference = [
['adding-a-package.md', '新增 Package', 'Adding a package'],
['adding-a-tool.md', '新增 Tool', 'Adding a tool'],
['adding-an-llm-adapter.md', '新增 LLM Adapter', 'Adding an LLM adapter'],
['adding-a-settings-card.md', '新增设置卡片', 'Adding a settings card'],
['adding-a-settings-card.md', '新增设置页', 'Adding a settings page'],
['extension-cookbook.md', '扩展模式', 'Extension patterns'],
] as const).map(([file, rootLabel, enLabel], order): PairedPage => ({
source: `docs/cookbook/${file}`,