docs(settings): scope the ordering claim and record the deferred wire gaps

Card registration order is stable only for cards one package installs
together: apply order between packages is unconstrained, so several
external cards can still reorder between boots. The note and README said
otherwise. Recorded alongside it: the redactor returns a secret reachable
only through a union, intersection, or transform verbatim, and serving
every registered namespace widens that gap to third-party schemas; and
the headline capability still lacks an assembled-composition test.

publish() now keeps its snapshot reference when neither the loaded flag
nor the dispatched namespaces moved, so an unrelated settings commit no
longer re-renders the card list.
This commit is contained in:
Yichen Jiang
2026-08-12 21:41:23 +08:00
parent 07d608a392
commit fdb1c47896
7 changed files with 20 additions and 8 deletions

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-plugin-config/README.md
README.md: 569f3a404f2b94fd6fb8dc5a4191cf66f37d55e2
README.zh.md: 5eb96dc07ba7a438824ce6c6c3da707dd3d26285
README.md: c4cda446b03d98eca01264a6a476999572f9e21f
README.zh.md: 9ddef246f1f80b310ac6f1b292cbd85f5306da49

View File

@@ -6,7 +6,7 @@ The **Plugins** settings section: one expandable card per Host plugin whose conf
## What appears here
The section reads which settings namespaces the Host serves and dispatches one slot key per namespace, so what renders is the intersection of two ledgers: the namespaces a live Host plugin registered, and the cards registered under those keys. A served namespace no card claims renders nothing — another surface owns it, or this deployment ships no browser half for it — and a card whose namespace this deployment does not serve is never dispatched, so an uncomposed plugin leaves no trace and does not hold the section back from its empty line. Cards appear in the order they registered, not the order the Host describes their namespaces — plugin activation can reorder the description between boots. The empty line waits for the Host's first answer, so an unanswered read never reads as "this deployment configures no plugin".
The section reads which settings namespaces the Host serves and dispatches one slot key per namespace, so what renders is the intersection of two ledgers: the namespaces a live Host plugin registered, and the cards registered under those keys. A served namespace no card claims renders nothing — another surface owns it, or this deployment ships no browser half for it — and a card whose namespace this deployment does not serve is never dispatched, so an uncomposed plugin leaves no trace and does not hold the section back from its empty line. Cards appear in the order they registered, which is stable for the cards one package installs together and not stable across plugins: apply order between packages is unconstrained. The empty line waits for the Host's first answer, so an unanswered read never reads as "this deployment configures no plugin".
The cards this package ships cover the shell executor (`bash`), the agent loop's tool-call parallelism (`agent-loop`), and the DeepSeek search provider (`web-search-deepseek`).

View File

@@ -6,7 +6,7 @@
## 这里会出现什么
本分区读取 Host 服务了哪些 settings 命名空间,并为每个命名空间派发一个 slot 键,因此渲染出来的是两份账本的交集:存活 Host 插件注册的命名空间,以及注册在这些键上的卡片。被服务却无人认领的命名空间什么都不渲染——它归别的界面所有,或本部署没有为它提供浏览器半侧;而命名空间未被本部署服务的卡片根本不会被派发,因此未组装的插件不留任何痕迹,也不会挡住那行空态文案。卡片按自身注册的顺序出现,而非 Host 描述其命名空间的顺序——插件激活时序会让后者在不同次启动之间变化。空态文案要等 Host 的第一次答复,因此一次尚未答复的读取绝不会被读成"本部署没有可配置的插件"。
本分区读取 Host 服务了哪些 settings 命名空间,并为每个命名空间派发一个 slot 键,因此渲染出来的是两份账本的交集:存活 Host 插件注册的命名空间,以及注册在这些键上的卡片。被服务却无人认领的命名空间什么都不渲染——它归别的界面所有,或本部署没有为它提供浏览器半侧;而命名空间未被本部署服务的卡片根本不会被派发,因此未组装的插件不留任何痕迹,也不会挡住那行空态文案。卡片按自身注册的顺序出现;对同一个包一起安装的卡片这是稳定的,对跨插件的卡片则不稳定:包与包之间的 apply 顺序是无约束的。空态文案要等 Host 的第一次答复,因此一次尚未答复的读取绝不会被读成"本部署没有可配置的插件"。
本包自带的卡片覆盖 shell 执行器(`bash`、agent 循环的工具调用并行度(`agent-loop`)以及 DeepSeek 搜索提供方(`web-search-deepseek`)。

View File

@@ -110,6 +110,14 @@ export class PluginConfigSectionController {
const served = new Set(this.served)
const namespaces = this.entries().flatMap(entry =>
entry.options.key !== undefined && served.has(entry.options.key) ? [entry.options.key] : [])
const previous = this.store.getSnapshot()
// Every settings-document commit re-reads, and most of them change nothing
// this section shows. An observable source must keep its snapshot
// reference until the fact moves, or each unrelated save re-renders the
// whole card list (packages/client/AGENTS.md reactive rule 5).
if (previous.loaded === this.loaded
&& previous.namespaces.length === namespaces.length
&& previous.namespaces.every((ns, index) => ns === namespaces[index])) return
this.store.set({ loaded: this.loaded, namespaces })
}
}