diff --git a/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.i18n.yaml
index 25b28406c4..67681ae7a7 100644
--- a/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.i18n.yaml
+++ b/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.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-08-04-configuration-source-ownership.md
-2026-08-04-configuration-source-ownership.md: 6dea1f08a9f2a0319f894a504082ff70bbe1118f
-2026-08-04-configuration-source-ownership.zh.md: 1ccf2c266c3e52f2f10d3bceb3b1505ae6266b15
+2026-08-04-configuration-source-ownership.md: b8c146ed76e500fcde3affd92bc5188b83083223
+2026-08-04-configuration-source-ownership.zh.md: 22afc15aec97a40829655fcb8179ad90a369bf0c
diff --git a/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.md b/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.md
index 6dea1f08a9..b8c146ed76 100644
--- a/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.md
+++ b/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.md
@@ -4,6 +4,8 @@ Status: implemented
English | [中文](2026-08-04-configuration-source-ownership.zh.md)
+The non-secret settings tier below is superseded by [profile-owned live configuration](2026-09-19-profile-owned-live-configuration.md). Credential ordering and environment restrictions remain current.
+
## Problem
`$DSH_HOME/.env` had just [become an ordinary environment layer](../../archived/architecture/2026-08-04-credentials-yaml-and-user-environment-layer.md), which left the harness resolving user-facing values from a flattened `process.env` that could no longer say where a value came from. Three consequences followed.
diff --git a/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.zh.md b/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.zh.md
index 1ccf2c266c..22afc15aec 100644
--- a/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.zh.md
+++ b/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.zh.md
@@ -4,6 +4,8 @@ Status: implemented
[English](2026-08-04-configuration-source-ownership.md) | 中文
+下文的非秘密 settings 层已由 [profile 所属实时配置](2026-09-19-profile-owned-live-configuration.zh.md) 替代。凭据优先级与环境限制仍然适用。
+
## Problem
`$DSH_HOME/.env` 刚刚[变成普通环境层](../../archived/architecture/2026-08-04-credentials-yaml-and-user-environment-layer.md),这使得 harness 解析面向用户的值时面对的是一个压平的 `process.env`,再也说不清某个值来自哪里。由此产生三个后果。
diff --git a/.agents/notes/implemented/architecture/2026-09-19-profile-owned-live-configuration.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-19-profile-owned-live-configuration.i18n.yaml
new file mode 100644
index 0000000000..8c966f2cfd
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-19-profile-owned-live-configuration.i18n.yaml
@@ -0,0 +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-19-profile-owned-live-configuration.md
+2026-09-19-profile-owned-live-configuration.md: 7acf10986987960a9748308c9fe19060a97cfcc0
+2026-09-19-profile-owned-live-configuration.zh.md: c9b88462a5e0a8601d9dcd83f7d82b49e7ce5b5c
diff --git a/.agents/notes/implemented/architecture/2026-09-19-profile-owned-live-configuration.md b/.agents/notes/implemented/architecture/2026-09-19-profile-owned-live-configuration.md
new file mode 100644
index 0000000000..7acf109869
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-19-profile-owned-live-configuration.md
@@ -0,0 +1,29 @@
+# Agent Note: Keep live configuration in the owning profile
+
+Status: implemented
+
+English | [中文](2026-09-19-profile-owned-live-configuration.zh.md)
+
+## Problem
+
+Separate settings registrations duplicated schemas, defaults, persistence, and notifications. They also placed stored user settings above deployment overlays without preserving which composition source supplied a value. Using the Loader's resolved Config gives validation and consumers one owner and preserves its lifecycle decisions.
+
+## Decision
+
+A plugin declares every configurable value in its Cordis Config. Fields that can change without remounting use `.volatile()`; consumers read their references during operations. Settings enumerates those fields for forms and never supplies business configuration. The profile editor writes `cordis.patch.yml` and applies the ordinary Loader reconciliation path.
+
+Form writes target the active profile. Home patches and command-line overlays remain higher-priority deployment inputs; an edit they would shadow fails before persistence. Two plugin instances are addressed by their distinct profile entry ids. Nested Includes retain independent ownership and are not writable through the profile form.
+
+Cordis config patches replace complete entry configs. An edit preserves ordinary fields and unedited secrets, including raw configuration expressions. A field reset restores the currently inherited value; resetting the complete entry removes its config overrides so later bundle changes are inherited. Other edits store the entry's complete config in the profile row: the ordinary fields as composed at write time plus every volatile field. Later bundle changes to those fields do not reach that profile until the entry's config override is removed. In the shipped bundles this pins `permission.presets` after a default-preset change, `agent-presets` after a preset choice, `agent-loop.agents` after a parallelism edit, and `web-search-deepseek.apiKeyEnv` after a Web Search edit, and the client marks every volatile field of such a row as overridden, not only the edited one. Narrowing a write to the edited fields needs merge semantics for patch `config`, which Include does not provide.
+
+The removed `$DSH_HOME/settings.yaml` is imported once into the active profile when the Loader has settled every entry after Settings starts: section ids are entry ids, with `ui-developer-tools` → `ui-settings`, `ui-onboarding` → `ui-settings-general`, and `shell` → the platform's shell executor entry. The file is renamed to `settings.yaml.imported` before the first write, so the import never repeats; a rejected section is logged and stays in the renamed file.
+
+## Alternatives considered
+
+The rejected alternative is retaining settings as a second store behind a configuration adapter: that still requires reconciling two durable documents and deciding which wins after concurrent edits or restart. The removed global settings document offered cross-profile preferences; this design deliberately makes persisted form values profile-specific. Reintroducing shared preferences requires an explicit shared Cordis layer and a defined write destination.
+
+## Consequences
+
+This supersedes only the non-secret settings tier in [configuration source ownership](2026-08-04-configuration-source-ownership.md); its environment restrictions and credential ordering remain applicable. [Volatile references](2026-09-18-volatile-config-references.md) own reference lifetime and ordinary update behavior.
+
+Real profile tests cover multiple instances, persisted restart, invalid edits, stale revisions, higher-layer rejection, secret redaction, and configuration expressions. Browser tests exercise form writes and reset through the assembled application. Runtime reference identity is checked across live edits.
diff --git a/.agents/notes/implemented/architecture/2026-09-19-profile-owned-live-configuration.zh.md b/.agents/notes/implemented/architecture/2026-09-19-profile-owned-live-configuration.zh.md
new file mode 100644
index 0000000000..c9b88462a5
--- /dev/null
+++ b/.agents/notes/implemented/architecture/2026-09-19-profile-owned-live-configuration.zh.md
@@ -0,0 +1,29 @@
+# Agent Note: 将实时配置保存在所属 profile 中
+
+Status: implemented
+
+[English](2026-09-19-profile-owned-live-configuration.md) | 中文
+
+## 问题
+
+独立的 settings 注册重复了 schema、默认值、持久化和通知,也让已存用户设置高于部署 overlay,却未保留值来自哪个组合来源。采用 Loader 已解析的 Config,使校验与消费者共享同一所有者,并保留已有生命周期判定。
+
+## 决定
+
+插件在 Cordis Config 中声明全部可配置值。无需重新挂载即可变化的字段使用 `.volatile()`;消费者在操作时读取其引用。Settings 只枚举这些字段并生成表单,不向业务提供配置。profile 编辑器写入 `cordis.patch.yml`,并通过普通 Loader 协调路径应用变化。
+
+表单写入当前 profile。home patch 和命令行 overlay 仍是优先级更高的部署输入;如果它们会遮蔽某次修改,编辑器会在持久化前拒绝写入。两个插件实例通过各自不同的 profile entry id 寻址。嵌套 Include 保留独立所有权,不能通过该 profile 表单写入。
+
+Cordis config patch 替换完整 entry config。编辑会保留普通字段、未编辑的秘密值和原始配置表达式。字段重置恢复当前继承值;整个 entry 重置后会移除其 config override,从而继承后续 bundle 变化。其他编辑会把该 entry 的完整 config 存入 profile 行:写入时组合出的普通字段加上全部 volatile 字段。此后 bundle 对这些字段的修改在该 entry 的 config override 被移除之前不会到达该 profile。在已发布的 bundle 中,修改默认权限预设会钉住 `permission.presets`,选择预设会钉住 `agent-presets`,编辑并行度会钉住 `agent-loop.agents`,编辑网页搜索会钉住 `web-search-deepseek.apiKeyEnv`;客户端会把这类行的每个 volatile 字段都标为已覆盖,而不只是被编辑的那个。要把写入收窄到被编辑字段需要 patch `config` 的合并语义,Include 不提供。
+
+被移除的 `$DSH_HOME/settings.yaml` 会在 Settings 启动后、Loader 完成所有条目加载时一次性导入当前 profile:section id 即 entry id,其中 `ui-developer-tools` → `ui-settings`、`ui-onboarding` → `ui-settings-general`、`shell` → 当前平台的 shell 执行器条目。文件在第一次写入前改名为 `settings.yaml.imported`,因此导入不会重复;被拒绝的 section 会记录日志并保留在改名后的文件中。
+
+## 考虑过的替代方案
+
+保留 settings 作为配置适配器背后的第二份存储仍需要协调两份持久化文档,并决定并发修改或重启后哪份优先,因此不采用。删除的全局 settings 文档曾提供跨 profile 偏好;本设计明确将表单值持久化到各 profile。若重新引入共享偏好,必须采用显式共享 Cordis 层并明确写入目标。
+
+## 后果
+
+本决定只替代 [配置来源所有权](2026-08-04-configuration-source-ownership.zh.md) 中的非秘密 settings 层;其中的环境限制和凭据优先级仍适用。[Volatile 引用](2026-09-18-volatile-config-references.zh.md) 负责引用生命周期和普通更新行为。
+
+真实 profile 测试覆盖多实例、持久化重启、非法编辑、过期修订、高优先级层拒绝、秘密脱敏及配置表达式。浏览器测试通过完整应用执行表单写入和重置,并检查实时编辑前后的运行时引用身份。
diff --git a/.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.i18n.yaml
index 1183d9cdc2..67810e388a 100644
--- a/.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.i18n.yaml
+++ b/.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.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/bug-fix/2026-08-06-host-backed-web-preferences.md
-2026-08-06-host-backed-web-preferences.md: 4ccd5cfe092e01fb266d51d5fc84b7cf1556214c
-2026-08-06-host-backed-web-preferences.zh.md: fbfdda432821eba1d32cab781340fd5f7802c61d
+2026-08-06-host-backed-web-preferences.md: 4b8d52887554899f2aee9253e65354e4c0fc7e46
+2026-08-06-host-backed-web-preferences.zh.md: 9c61de71981c9e4950889cf94eba2645b9247aa8
diff --git a/.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.md b/.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.md
index 4ccd5cfe09..4b8d528875 100644
--- a/.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.md
+++ b/.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.md
@@ -12,11 +12,11 @@ The first theme implementation moved only Appearance to Host settings but awaite
## Decision
-The owning Host halves register three schemas: optional `locale.preference` (`zh` or `en`, where absence delegates to the browser), `ui-theme.preference` (`light`, `dark`, or `system`, default `system`), and `ui-conversation.busyEnter` (`queue` or `steer`, default `queue`). The local settings provider stores explicit choices in `$DSH_HOME/settings.yaml`, which resolves to `~/.dsh/settings.yaml` under the default home. The API proxy serves every registered namespace behind Connection's browser authentication; field roles still redact secrets.
+The owning Host Config schemas declare volatile locale, theme, and busy-Enter preferences. ConfigEditor stores explicit choices in the active profile patch. The authenticated settings API projects those Config fields and redacts secret roles.
-`dsh-client-ui-settings` owns one browser-wide settings describe mirror and provides `ctx.settingsScope.bind(spec)` as a per-namespace selector over it. The mirror installs `settings/document-updated` and `connection/reset` listeners before starting its background read, so no settings transport can block plugin activation and an invalidation cannot fall into a read-before-subscribe gap. Each bound scope publishes a snapshot store (status, section value, revision, writability, host/memory mode) the domain service subscribes to, without adding a wire read or listener of its own. The default decoder validates each incoming section against the namespace's own serialized wire schema, rehydrated through the colocated `ctx.settingsSchema` service, so domains carry no hand-written wire guards. Domain services take the scope as an ordinary constructor collaborator, publish their provisional defaults immediately—browser-derived locale, system theme, and Queue—then adopt an accepted Host section without writing it back; a service constructed without a scope (standalone dictionary or policy fixtures) simply stays process-local. The shared read and invalidation lifecycle is specified by the later [settings describe mirror decision](../../archived/architecture/2026-08-17-settings-describe-mirror.md).
+`dsh-client-ui-settings` owns one browser-wide settings describe mirror and provides `ctx.configForms.get(entryId)` as a per-namespace selector over it. The mirror installs `settings/document-updated` and `connection/reset` listeners before starting its background read, so no settings transport can block plugin activation and an invalidation cannot fall into a read-before-subscribe gap. Each shared entry form publishes a snapshot store (status, section value, revision, writability, host/memory mode) the domain service subscribes to, without adding a wire read or listener of its own. The default decoder validates each incoming section against the namespace's own serialized wire schema, rehydrated through the colocated `ctx.settingsSchema` service, so domains carry no hand-written wire guards. Domain services take the scope as an ordinary constructor collaborator, publish their provisional defaults immediately—browser-derived locale, system theme, and Queue—then adopt an accepted Host section without writing it back; a service constructed without a scope (standalone dictionary or policy fixtures) simply stays process-local. The shared read and invalidation lifecycle is specified by the later [settings describe mirror decision](../../archived/architecture/2026-08-17-settings-describe-mirror.md).
-User changes update the live service synchronously and queue a `settings.mutate` path operation through `scope.set`. The scope serializes gestures, sends the latest known namespace revision as `expectedRevision`, records every successful revision, and lets only the latest write settlement republish live state. A rejected or failed latest write reloads Host state. Disposal rejects new work, skips queued operations, suppresses publication by the in-flight operation, and waits for that operation to settle before the plugin reaches quiescence.
+User changes update the live preference immediately and queue a revision-fenced mutation through the shared entry form. The provider owns one write queue per entry; consumers release their subscriptions when they unload. Provider disposal skips queued work, suppresses late publication, and waits for the in-flight operation.
The Client keeps Host persistence disabled on non-loopback pages, so their preferences remain process-local even though Connection authenticates the complete API. Dynamic third-party theme ids remain in-process extensions outside the built-in Host schema; removing one resets the live registry without replacing the last durable built-in preference.
@@ -36,7 +36,7 @@ The Client keeps Host persistence disabled on non-loopback pages, so their prefe
## Consequences
-Appearance, Language, and busy-Enter choices follow the DSH user home across reloads, ports, and loopback origins. Direct edits to `settings.yaml` converge through the existing invalidation stream, while legacy `dsh.theme`, `dsh.locale`, and `dsh.conversation.busyEnter` entries are neither read nor written.
+Appearance, Language, and busy-Enter choices follow the active profile across reloads, ports, and loopback origins. ConfigEditor and volatile HMR apply profile changes; the form mirror follows accepted values through settings invalidations. Legacy browser preference keys remain unused.
Boot may briefly show the domain default before the background read settles. A transient read failure keeps that default or the last good in-process value; reconnect retries. A write rejection can visibly restore the durable preference after the immediate local change.
diff --git a/.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.zh.md b/.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.zh.md
index fbfdda4328..9c61de7198 100644
--- a/.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.zh.md
+++ b/.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.zh.md
@@ -12,11 +12,11 @@ Web 的 Appearance、Language 和繁忙态 Enter 偏好原本存在浏览器 `lo
## 决策
-各领域所属的 Host half 注册三份 schema:可选的 `locale.preference`(`zh` 或 `en`,缺失时交由浏览器决定)、`ui-theme.preference`(`light`、`dark` 或 `system`,默认为 `system`),以及 `ui-conversation.busyEnter`(`queue` 或 `steer`,默认为 `queue`)。本地 settings 提供方将显式选择存入 `$DSH_HOME/settings.yaml`,在使用默认 home 时,该路径解析为 `~/.dsh/settings.yaml`。API 代理在 Connection 浏览器认证之后服务每一个已注册的 namespace;字段角色仍会脱敏机密值。
+对应 Host Config schema 声明 volatile 语言、主题和忙碌时 Enter 偏好。ConfigEditor 将显式选择保存到活跃 profile patch。经过认证的 settings API 投影这些 Config 字段,并对 secret 角色字段脱敏。
-`dsh-client-ui-settings` 持有一个浏览器全局的 settings describe 镜像,并提供 `ctx.settingsScope.bind(spec)` 作为该镜像上的逐 namespace selector。镜像在开始后台读取之前安装 `settings/document-updated` 和 `connection/reset` 监听器,因此任何 settings 传输都不会阻塞插件激活,失效通知也不会掉入先读取、后订阅的空档。每个绑定的 scope 会发布一个供领域服务订阅的快照 store(状态、分节值、revision、可写性、host/内存模式),自身不再增加协议读取或监听器。默认解码器会对照该 namespace 自身的序列化 wire schema(经同包的 `ctx.settingsSchema` 服务还原)校验每个传入分节,因此各领域无需携带手写的 wire 校验器。领域服务把 scope 当作普通的构造函数协作者接收,立即发布各自的暂定默认值:由浏览器派生的 locale、系统主题和 Queue;随后采纳已获接受的 Host 分节,但不将其写回;不带 scope 构造的服务——独立词典或政策 fixture(测试前置数据)——则仅停留在进程本地。共享读取与失效生命周期由后续的 [settings describe 镜像决策](../../archived/architecture/2026-08-17-settings-describe-mirror.md)规定。
+`dsh-client-ui-settings` 持有一个浏览器全局的 settings describe 镜像,并提供 `ctx.configForms.get(entryId)` 作为该镜像上的逐 namespace selector。镜像在开始后台读取之前安装 `settings/document-updated` 和 `connection/reset` 监听器,因此任何 settings 传输都不会阻塞插件激活,失效通知也不会掉入先读取、后订阅的空档。每个共享条目表单会发布一个供领域服务订阅的快照 store(状态、分节值、revision、可写性、host/内存模式),自身不再增加协议读取或监听器。默认解码器会对照该 namespace 自身的序列化 wire schema(经同包的 `ctx.settingsSchema` 服务还原)校验每个传入分节,因此各领域无需携带手写的 wire 校验器。领域服务把 scope 当作普通的构造函数协作者接收,立即发布各自的暂定默认值:由浏览器派生的 locale、系统主题和 Queue;随后采纳已获接受的 Host 分节,但不将其写回;不带 scope 构造的服务——独立词典或政策 fixture(测试前置数据)——则仅停留在进程本地。共享读取与失效生命周期由后续的 [settings describe 镜像决策](../../archived/architecture/2026-08-17-settings-describe-mirror.md)规定。
-用户变更会同步更新实时服务,并经 `scope.set` 将一项 `settings.mutate` 路径操作排入队列。scope 会串行处理手势,以最新已知 namespace revision 作为 `expectedRevision` 发送,记录每次成功写入的 revision,并且只允许最新写入的结算结果重新发布实时状态。最新写入被拒或失败时,scope 会重新加载 Host 状态。插件释放会拒绝新工作、跳过已排队操作、抑制运行中操作发布状态,并等待该操作结算后才让插件达到完全停稳。
+用户修改立即更新实时偏好,并通过共享条目表单排队提交带修订号检查的修改。提供者为每个条目持有一个写入队列;消费者在卸载时释放订阅。提供者销毁时跳过排队工作、禁止迟到发布,并等待正在执行的操作结束。
Client 在非 loopback 页面禁用 Host 持久化,因此这些页面的偏好仍只保留在进程内,尽管 Connection 认证完整 API。动态第三方主题 id 仍是内置 Host schema 之外的进程内扩展;移除其中一个会重置实时注册表,但不会替换上一个持久化的内置偏好。
@@ -36,7 +36,7 @@ Client 在非 loopback 页面禁用 Host 持久化,因此这些页面的偏好
## 后果
-Appearance、Language 和繁忙态 Enter 选择会跟随 DSH 用户 home,跨越重新加载、端口与回环 origin。直接编辑 `settings.yaml` 所产生的变更会通过现有失效流收敛,而旧的 `dsh.theme`、`dsh.locale` 和 `dsh.conversation.busyEnter` 条目既不会被读取,也不会被写入。
+外观、语言和忙碌时 Enter 偏好跟随活跃 profile,跨刷新、端口和回环 origin 保持一致。ConfigEditor 和 volatile HMR 应用 profile 更改;表单镜像通过 settings 失效通知跟随已接受值。旧浏览器偏好键保持不使用。
启动时可能会在后台读取结算前短暂显示领域默认值。短暂的读取失败会保留该默认值或上一个正确的进程内值;重连时会重试。写入被拒时,界面可能会在本地值立即变化后明显恢复为持久化偏好。
diff --git a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.i18n.yaml b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.i18n.yaml
index af257a325c..6485deaade 100644
--- a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-09-17-developer-tools-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 .agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md
-2026-09-17-developer-tools-settings.md: 7819b2a2df3c9eaf2db1e1271d7b419768affaff
-2026-09-17-developer-tools-settings.zh.md: 5ff9f2c1e2380f086969cc392bf649edd13e2893
+2026-09-17-developer-tools-settings.md: 5ae8861efa701042e0730310ddb23f6f559d14d7
+2026-09-17-developer-tools-settings.zh.md: e7a9d3c28d53ba9fe14c970bd48f2a6031248e58
diff --git a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md
index 7819b2a2df..5ae8861efa 100644
--- a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md
+++ b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.md
@@ -10,7 +10,7 @@ Diagnostic views, preset selection and changed-file summaries add complexity to
## Decision
-The settings-owned `ctx.settingsScope.developerTools` preference binds a namespace scope and exposes one accepted `ui-developer-tools.enabled` preference. The initial default-off choice reduces routine-task complexity; [Developer tools default on after preference resolution](2026-09-20-developer-tools-default-on.md) supersedes that default and owns startup enablement. General Settings offers the same switch on Web and desktop. The existing settings transport owns validation, ordered writes, persistence and recovery; loopback Web and desktop follow Host acceptance. Remote Web owns one browser-local observable shared by all consumers until reload, with no Host writes.
+The settings-owned `ctx.configForms.developerTools` preference uses a shared entry form and exposes one accepted `ui-settings.enabled` preference. The initial default-off choice reduces routine-task complexity; [Developer tools default on after preference resolution](2026-09-20-developer-tools-default-on.md) supersedes that default and owns startup enablement. General Settings offers the same switch on Web and desktop. The existing settings transport owns validation, ordered writes, persistence and recovery; loopback Web and desktop follow Host acceptance. Remote Web owns one browser-local observable shared by all consumers until reload, with no Host writes.
Off hides the Trajectory View, hides tool Inspect actions when no visible View declares tool-call inspection, hides the conversation View tab bar while fewer than two Views are available, hides the new-session preset chip and the settings row that allows switching Agent modes, and omits the changed-files card and its summary read. Views registered by other plugins stay available. Turning it off while Trajectory is active activates Chat. The preset composition, saved default, Session events and explicit delivery cards remain unchanged. This preference is presentation policy, not Host authorization.
diff --git a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.zh.md b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.zh.md
index 5ff9f2c1e2..e7a9d3c28d 100644
--- a/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.zh.md
+++ b/.agents/notes/implemented/feature/2026-09-17-developer-tools-settings.zh.md
@@ -10,7 +10,7 @@ Status: implemented
## Decision
-设置域内的 `ctx.settingsScope.developerTools` 偏好对象绑定命名空间 scope,提供一个已接受的 `ui-developer-tools.enabled` 偏好。最初默认关闭的选择用于降低日常任务的复杂度;[偏好解析后默认开启开发者工具](2026-09-20-developer-tools-default-on.zh.md)取代该默认值并负责启动时的启用策略。Web 和桌面端的通用设置提供同一个开关。现有设置传输负责验证、有序写入、持久化和恢复;回环 Web 和桌面端跟随 Host 接受的状态。远程 Web 持有一个所有消费者共享的浏览器本地可观察值,刷新后重置,不发送 Host 写入。
+设置域内的 `ctx.configForms.developerTools` 偏好对象使用共享条目表单,提供一个已接受的 `ui-settings.enabled` 偏好。最初默认关闭的选择用于降低日常任务的复杂度;[偏好解析后默认开启开发者工具](2026-09-20-developer-tools-default-on.zh.md)取代该默认值并负责启动时的启用策略。Web 和桌面端的通用设置提供同一个开关。现有设置传输负责验证、有序写入、持久化和恢复;回环 Web 和桌面端跟随 Host 接受的状态。远程 Web 持有一个所有消费者共享的浏览器本地可观察值,刷新后重置,不发送 Host 写入。
关闭时隐藏 Trajectory View,在没有可见 View 声明工具调用检查能力时隐藏 Inspect 操作,可见 View 少于两个时隐藏会话 View 标签栏,隐藏新会话预设入口和「允许切换agent模式」设置行,并省略改动文件卡片及其摘要读取。其他插件注册的 View 保持可用。在 Trajectory 中关闭开关会激活 Chat。预设组合、保存的默认值、Session 事件和显式交付卡片保持不变。此偏好是展示策略,不是 Host 授权。
diff --git a/apps/cli/composition.md b/apps/cli/composition.md
index fb7a946d7c..1723cb9d6b 100644
--- a/apps/cli/composition.md
+++ b/apps/cli/composition.md
@@ -46,7 +46,9 @@ flowchart LR
cfg --> plugin_dsh_base_jobs
plugin_dsh_base_llm_retry["llm-retry
@deepseek-ai/dsh-llm-retry"]
cfg --> plugin_dsh_base_llm_retry
- plugin_dsh_base_settings["settings
@deepseek-ai/dsh-settings-file"]
+ plugin_dsh_base_config_editor["config-editor
@deepseek-ai/dsh-config-editor"]
+ cfg --> plugin_dsh_base_config_editor
+ plugin_dsh_base_settings["settings
@deepseek-ai/dsh-settings"]
cfg --> plugin_dsh_base_settings
plugin_dsh_base_credentials["credentials
@deepseek-ai/dsh-credentials-local"]
cfg --> plugin_dsh_base_credentials
@@ -209,7 +211,8 @@ flowchart LR
| `agent-default-model` | `@deepseek-ai/dsh-agent-default-model` |
| `jobs` | `@deepseek-ai/dsh-jobs-local` |
| `llm-retry` | `@deepseek-ai/dsh-llm-retry` |
-| `settings` | `@deepseek-ai/dsh-settings-file` |
+| `config-editor` | `@deepseek-ai/dsh-config-editor` |
+| `settings` | `@deepseek-ai/dsh-settings` |
| `credentials` | `@deepseek-ai/dsh-credentials-local` |
| `llm-pi-ai` | `@deepseek-ai/dsh-llm-pi-ai` |
| `session-persistence-jsonl` | `@deepseek-ai/dsh-session-persistence-jsonl` |
diff --git a/apps/cli/package.json b/apps/cli/package.json
index 0c9cea1db0..b1efe06eeb 100644
--- a/apps/cli/package.json
+++ b/apps/cli/package.json
@@ -134,7 +134,6 @@
"@deepseek-ai/dsh-session-query": "workspace:^",
"@deepseek-ai/dsh-shell-env": "workspace:^",
"@deepseek-ai/dsh-settings": "workspace:^",
- "@deepseek-ai/dsh-settings-file": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-subagent-fork-in-process": "workspace:^",
"@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:^",
diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts
index 7151747b4e..3afc7622c1 100644
--- a/apps/cli/tests/web-agent-presets.e2e.ts
+++ b/apps/cli/tests/web-agent-presets.e2e.ts
@@ -19,11 +19,12 @@ import { SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
-import { SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-tool-subagent/model-selection-settings'
import { dump, load } from 'js-yaml'
import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
import { bundlePatchPaths, composeEntries } from '@deepseek-ai/dsh-app-boot'
-const SETTINGS_NAMESPACE = 'agent-presets'
+/** Profile entry ids whose volatile fields these scenarios edit through Settings. */
+const SETTINGS_NAMESPACE = 'agent-preset-registry'
+const SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE = 'subagent-model-selection-settings'
import { applyChildComposition, childSessionMeta } from '@deepseek-ai/dsh-subagent'
import { ToolCallId } from '@deepseek-ai/dsh-llm'
import type {} from '@deepseek-ai/dsh-compaction-basic'
@@ -59,25 +60,20 @@ const MINIMAL_BASH_DESCRIPTION = `Run commands in a bash shell
* agent's capabilities is the real thing, including both shipped presets.
*/
async function bootWeb(
- settingsFile: string,
+ profileHome: string,
extra: PatchOptions[] = [],
profilePackages: readonly string[] = [],
profileBundles?: readonly string[],
): Promise {
- const storageRoot = join(dirname(settingsFile), 'storages')
+ const storageRoot = join(profileHome, 'storages')
const overrides: PatchOptions[] = [
- // The settings row defaults to `$DSH_HOME/settings.yaml`. Left alone it
- // reads the developer's own document — and since the default preset is a
- // setting, a stored `agent-presets.default` would decide this file's
- // outcome. Point it at a temporary file to isolate settings.
- { id: 'settings', config: { path: settingsFile, watch: false } },
// storage-json's root is anchored to the real $DSH_HOME. Unpinned, this
// file writes the developer's own `~/.dsh/storages/` — and then reads it
// back on the next run, so a stored document from any other build decides
- // this test's boot. Same reason the settings row above is pinned.
+ // this test's boot.
{ id: 'storage-json', config: { root: storageRoot } },
// Fixed Session IDs must stay inside this boot's temporary profile root.
- { id: 'session-persistence-jsonl', config: { root: join(dirname(settingsFile), 'sessions') } },
+ { id: 'session-persistence-jsonl', config: { root: join(profileHome, 'sessions') } },
// Host rows with side effects outside this process: a bound port, a served
// asset tree, a telemetry exporter. `api-gateway` and `directory-picker`
// stay ENABLED on purpose — the api-proxy is the host row that injects
@@ -123,7 +119,7 @@ async function bootWeb(
{ id: 'agent-preset-registry', config: { default: 'standard' } },
...extra,
]
- const home = dirname(settingsFile)
+ const home = profileHome
const profileDir = join(home, 'profiles', 'spec')
await mkdir(profileDir, { recursive: true })
if (profileBundles === undefined) initProfile(profileDir, ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'])
@@ -157,9 +153,15 @@ async function bootWeb(
profile = loadProfile('dsh-test', 'spec', INSTALL_ANCHOR, home, { userLayer: false })
bundlePatches = profile.layers.flatMap(layer => layer.patches)
}
- const declarations = overrides.filter(patch => 'insert' in patch)
- await writeFile(profile.patchPath, dump(declarations, { schema: entryListSchema }))
- const launchOverrides = overrides.filter(patch => !('insert' in patch))
+ // Deployment defaults live in a bundle beneath the profile patch, so Settings writes are not shadowed by overlays.
+ const fixtureName = 'dsh-web-presets-defaults'
+ const fixtureDir = join(profileDir, 'node_modules', fixtureName)
+ await mkdir(fixtureDir, { recursive: true })
+ await writeFile(join(fixtureDir, 'package.json'), JSON.stringify({ name: fixtureName, version: '1.0.0', dsh: { bundle: { patch: 'cordis.patch.yml' } } }))
+ await writeFile(join(fixtureDir, 'cordis.patch.yml'), JSON.stringify(overrides))
+ const manifest = JSON.parse(await readFile(join(profileDir, 'package.json'), 'utf8')) as { dsh: { profile: { bundles: string[] } } }
+ manifest.dsh.profile.bundles.push(fixtureName)
+ await writeFile(join(profileDir, 'package.json'), JSON.stringify(manifest))
const resolution = await createRuntimeResolution({ installAnchor: INSTALL_ANCHOR, home, profile })
const rootConfig = join(profileDir, 'cordis.yml')
await writeFile(rootConfig, '[]\n')
@@ -167,7 +169,7 @@ async function bootWeb(
bootCtx.provide('profileContext', { name: 'spec', dir: profileDir, patchPath: profile.patchPath,
installAnchor: INSTALL_ANCHOR, home, cwd: home,
startedBundles: profileBundles ?? ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'],
- overlays: launchOverrides, telemetryDisabledEnv: '1' })
+ overlays: [], telemetryDisabledEnv: '1' })
await bootCtx.plugin(PluginPackages, { resolution })
bootCtx.provide('connection', {
fetch: { register: () => () => {} },
@@ -202,9 +204,7 @@ function enablePresetTool(composition: string, id: string): string {
let ctx: Context
beforeAll(async () => {
- const settingsFile = join(await mkdtemp(join(tmpdir(), 'dsh-web-presets-')), 'settings.yaml')
- await writeFile(settingsFile, '{}\n')
- ctx = await bootWeb(settingsFile)
+ ctx = await bootWeb(await mkdtemp(join(tmpdir(), 'dsh-web-presets-')))
}, 120_000)
describe('the shipped Web composition', () => {
@@ -544,10 +544,8 @@ describe('product Bundle and user-preset intersection', () => {
async function bootProducts(installed: readonly Product[]): Promise {
const root = await mkdtemp(join(tmpdir(), 'dsh-product-presets-'))
const definitions: import('@deepseek-ai/cordis-plugin-loader').EntryOptions[] = []
- const settingsFile = join(root, 'settings.yaml')
const standardConfig = composeEntries([webPatches('test')]).find(row => row.id === 'preset-standard')!.config as import('@deepseek-ai/dsh-agent-preset-registry').PresetDefinition
const standard = dump(standardConfig.plugins, { schema: entryListSchema })
- await writeFile(settingsFile, '{}\n')
for (const id of presetIds) {
let composition = standard
if (id === 'products-codex' || id === 'products-both') {
@@ -566,7 +564,7 @@ describe('product Bundle and user-preset intersection', () => {
? '@deepseek-ai/dsh-subagent-codex'
: '@deepseek-ai/dsh-subagent-claude-code'
)
- return await bootWeb(settingsFile, [{ insert: definitions } ], installed.map(packageDir), [
+ return await bootWeb(root, [{ insert: definitions }], installed.map(packageDir), [
'@deepseek-ai/dsh-base',
'@deepseek-ai/dsh-web-app',
...installed.map(packageName),
@@ -639,10 +637,8 @@ describe('a user preset declared from the shipped cordis rows', () => {
// `tool-cordis` row only registers the tools. Before that split the copy
// failed to mount: its row re-registered provider "Service".
const root = await mkdtemp(join(tmpdir(), 'dsh-copied-preset-'))
- const settingsFile = join(root, 'settings.yaml')
- await writeFile(settingsFile, '{}\n')
const cordis = composeEntries([webPatches('test')]).find(row => row.id === 'preset-cordis')!.config as import('@deepseek-ai/dsh-agent-preset-registry').PresetDefinition
- const copyCtx = await bootWeb(settingsFile, [{ insert: [{
+ const copyCtx = await bootWeb(root, [{ insert: [{
id: 'preset-cordis-copy', name: '@deepseek-ai/dsh-agent-preset',
config: { ...cordis, id: 'cordis-copy', name: 'Cordis copy' },
}] }])
@@ -814,7 +810,7 @@ describe('the default preset as a user setting', () => {
expect((await ctx.agentPresets.remoteExportList()).modeSelectionEnabled).toBe(true)
expect(ctx.agentPresets.defaultId).toBe('standard')
- await ctx.settings.update(SETTINGS_NAMESPACE, { default: 'minimal' })
+ await ctx.settings.update(SETTINGS_NAMESPACE, { selectedDefault: 'minimal' })
try {
expect(ctx.agentPresets.defaultId).toBe('minimal')
diff --git a/apps/web/tests/assembled-remote.ts b/apps/web/tests/assembled-remote.ts
index b67b8c9837..fd617ba974 100644
--- a/apps/web/tests/assembled-remote.ts
+++ b/apps/web/tests/assembled-remote.ts
@@ -144,9 +144,9 @@ export function createAssembledRemote(options: AssembledRemoteOptions = {}): Ass
'settings/describe': ok({
...fixture.settingsDescribe.value,
namespaces: [...fixture.settingsDescribe.value.namespaces, {
- ns: 'ui-developer-tools',
+ ns: 'ui-settings',
schema: { type: 'object', dict: { enabled: { type: 'boolean' } } },
- value: { enabled: options.developerTools ?? true }, applies: 'live', secrets: [], revision: 0,
+ value: { enabled: options.developerTools ?? true }, autoGenerate: false, applies: 'live', secrets: [], revision: 0,
}],
}),
'credentials/describe': structuredClone(fixture.credentialsDescribe),
diff --git a/apps/web/tests/clickable-links-gallery.e2e.ts b/apps/web/tests/clickable-links-gallery.e2e.ts
index 2cf02b02eb..380ca8fd2e 100644
--- a/apps/web/tests/clickable-links-gallery.e2e.ts
+++ b/apps/web/tests/clickable-links-gallery.e2e.ts
@@ -458,7 +458,7 @@ describe('web e2e: clickable links gallery', () => {
await page.getByRole('button', { name: 'Settings', exact: true }).click()
await page.getByRole('button', { name: 'Built-in browser', exact: true }).click()
await page.getByRole('menuitem', { name: 'New browser tab', exact: true }).click()
- await expect.poll(() => scaffold.ctx.settings.get('ui-chat')).toMatchObject({ linkOpening: 'new-tab' })
+ await expect.poll(() => scaffold.ctx.settings.describe().find(row => row.ns === 'ui-chat')?.value).toMatchObject({ linkOpening: 'new-tab' })
await page.keyboard.press('Escape')
const popupPromise = page.waitForEvent('popup')
await guideLink.click()
@@ -475,7 +475,7 @@ describe('web e2e: clickable links gallery', () => {
await page.getByRole('button', { name: 'Settings', exact: true }).click()
await page.getByRole('button', { name: 'New browser tab', exact: true }).click()
await page.getByRole('menuitem', { name: 'Built-in browser', exact: true }).click()
- await expect.poll(() => scaffold.ctx.settings.get('ui-chat')).toMatchObject({ linkOpening: 'sidebar' })
+ await expect.poll(() => scaffold.ctx.settings.describe().find(row => row.ns === 'ui-chat')?.value).toMatchObject({ linkOpening: 'sidebar' })
await page.keyboard.press('Escape')
await guideLink.click()
await expect.poll(() => browserAddress.inputValue()).toBe(GUIDE_URL)
diff --git a/apps/web/tests/declared-reasoning.e2e.ts b/apps/web/tests/declared-reasoning.e2e.ts
index 599d98acbf..897e192cab 100644
--- a/apps/web/tests/declared-reasoning.e2e.ts
+++ b/apps/web/tests/declared-reasoning.e2e.ts
@@ -99,7 +99,7 @@ describe.skipIf(MODE === 'record')('web e2e: declared reasoning efforts reach th
await page.keyboard.press('Tab')
await expect.poll(() => levels.count(), { timeout: 10_000 }).toBe(0)
await expect.poll(
- async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'),
+ async () => readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8'),
{ timeout: 10_000 },
).toContain('reasoningEffort: high')
await expect.poll(() => trigger.getAttribute('aria-label'), { timeout: 10_000 })
diff --git a/apps/web/tests/deepseek-messages-settings.e2e.ts b/apps/web/tests/deepseek-messages-settings.e2e.ts
index 3018cc432e..5114d85c95 100644
--- a/apps/web/tests/deepseek-messages-settings.e2e.ts
+++ b/apps/web/tests/deepseek-messages-settings.e2e.ts
@@ -62,13 +62,13 @@ describe.skipIf(webSnapshotMode() === 'record')('web e2e: DeepSeek Messages sett
await messages.getByRole('button', { name: '保存', exact: true }).click()
await dialog.getByText('已保存 DeepSeek (deepseek-official)。', { exact: true }).waitFor()
- const settings = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
+ const settings = await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')
expect(settings).toContain('https://messages.example/anthropic')
- expect(settings).toContain('llm-deepseek:')
+ expect(settings).toContain('id: llm-deepseek')
await expect(scaffold.ctx.llm.resolveModelInfo('deepseek-official', 'deepseek-flash')).resolves.toMatchObject({
name: 'Messages Flash', inputModalities: ['text', 'image'], systemPromptUpdate: 'in-history',
})
- expect(scaffold.ctx.settings.get('llm-deepseek')).not.toHaveProperty('protocol')
+ expect(scaffold.ctx.settings.describe().find(row => row.ns === 'llm-deepseek')?.value).not.toHaveProperty('protocol')
expect(settings).not.toContain('sk-e2e-')
const credentials = await readFile(join(scaffold.harnessHome, '.credentials.yaml'), 'utf8')
expect(credentials).toContain('DEEPSEEK_API_KEY: sk-e2e-messages')
@@ -96,7 +96,7 @@ describe.skipIf(webSnapshotMode() === 'record')('web e2e: DeepSeek Messages sett
await page.getByRole('menuitemradio', { name: 'Messages Flash', exact: true }).click()
await expect.poll(() => input.isEnabled()).toBe(true)
await expect.poll(() => scaffold.ctx.agentDefaultModel.currentSelection().provider).toBe('deepseek-official')
- const settings = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
+ const settings = await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')
expect(settings).toContain('provider: deepseek-official')
expect(tripwire.pageErrors).toEqual([])
}, 60_000)
diff --git a/apps/web/tests/default-model.e2e.ts b/apps/web/tests/default-model.e2e.ts
index 304bdb97f6..474afb1c36 100644
--- a/apps/web/tests/default-model.e2e.ts
+++ b/apps/web/tests/default-model.e2e.ts
@@ -111,10 +111,10 @@ describe('web e2e: the composer model switch is the default for later sessions',
// The switch is what sets the default: the shared Agent-route settings section
// now names it, beside the provider profiles the Models page writes.
await expect.poll(
- async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'),
+ async () => readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8'),
{ timeout: 10_000 },
- ).toContain('agent-default-model:')
- const document = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
+ ).toContain('id: agent-default-model')
+ const document = await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')
expect(document).toContain(`provider: ${ROUTE}`)
expect(document).toContain(`model: ${MODEL}`)
diff --git a/apps/web/tests/developer-tools-settings.e2e.ts b/apps/web/tests/developer-tools-settings.e2e.ts
index 889db40d16..d654308b77 100644
--- a/apps/web/tests/developer-tools-settings.e2e.ts
+++ b/apps/web/tests/developer-tools-settings.e2e.ts
@@ -16,10 +16,10 @@ it('persists developer tools in the Host settings document and restores the acce
await page.getByRole('button', { name: 'Settings', exact: true }).click()
const toggle = page.getByRole('switch', { name: 'Developer tools' })
expect(await toggle.getAttribute('aria-checked')).toBe('true')
- expect(await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')).not.toContain('ui-developer-tools:')
+ expect(await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')).not.toMatch(/id: ui-settings(?:\r?\n|$)/)
await toggle.click()
await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('false')
- expect(await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')).toContain('ui-developer-tools:\n enabled: false')
+ expect(await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')).toContain('enabled: false')
await page.reload()
await page.getByRole('button', { name: 'Settings', exact: true }).click()
await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('false')
@@ -28,13 +28,13 @@ it('persists developer tools in the Host settings document and restores the acce
const pickerToggle = page.getByRole('switch', { name: 'Choose a mode for new tasks' })
await expect.poll(() => page.getByRole('heading', { name: 'Agent presets', exact: true }).count()).toBe(1)
expect(await pickerToggle.count()).toBe(0)
- await scaffold.ctx.settings.update('ui-developer-tools', { enabled: true })
+ await scaffold.ctx.settings.update('ui-settings', { enabled: true })
await expect.poll(() => pickerToggle.count()).toBe(1)
await expect.poll(() => pickerToggle.isEnabled()).toBe(true)
const pickerEnabled = await pickerToggle.getAttribute('aria-checked')
- await scaffold.ctx.settings.update('ui-developer-tools', { enabled: false })
+ await scaffold.ctx.settings.update('ui-settings', { enabled: false })
await expect.poll(() => pickerToggle.count()).toBe(0)
- await scaffold.ctx.settings.update('ui-developer-tools', { enabled: true })
+ await scaffold.ctx.settings.update('ui-settings', { enabled: true })
await expect.poll(() => pickerToggle.isEnabled()).toBe(true)
await expect.poll(() => pickerToggle.getAttribute('aria-checked')).toBe(pickerEnabled)
})
diff --git a/apps/web/tests/document-preview.e2e.ts b/apps/web/tests/document-preview.e2e.ts
index 9a8a83f37c..c20666b429 100644
--- a/apps/web/tests/document-preview.e2e.ts
+++ b/apps/web/tests/document-preview.e2e.ts
@@ -168,7 +168,7 @@ else process.exit(1);
await saveFailureShot(page, `screenshots/0908-document-preview/smoke-${process.pid}`)
})
expect(await page.locator('[data-slot="conversation.hero.agentPreset"] button').count()).toBe(0)
- expect(scaffold.ctx.settings.get('ui-developer-tools')).toEqual({ enabled: false })
+ expect(scaffold.ctx.settings.describe().find(row => row.ns === 'ui-settings')?.value).toEqual({ enabled: false })
const settled = scaffold.whenTurnSettled()
const input = page.locator('[data-composer-input]').first()
await input.fill(PROMPT)
@@ -444,11 +444,11 @@ else process.exit(1);
await expect.poll(() => html.locator('#local-result').innerText()).toBe('LOCAL_JS_REFRESHED')
expect(await iframe.getAttribute('src')).not.toBe(beforeScriptSave)
await page.getByRole('tab', { name: /Trajectory/ }).click()
- await scaffold.ctx.settings.update('ui-developer-tools', { enabled: false })
+ await scaffold.ctx.settings.update('ui-settings', { enabled: false })
await expect.poll(() => page.getByRole('tab', { name: /Trajectory/ }).count()).toBe(0)
await expect.poll(() => iframe.getAttribute('sandbox')).toBe('')
expect(await page.getByText('LIGHTHOUSE', { exact: true }).count()).toBeGreaterThan(0)
- await scaffold.ctx.settings.update('ui-developer-tools', { enabled: true })
+ await scaffold.ctx.settings.update('ui-settings', { enabled: true })
await expect.poll(() => iframe.getAttribute('sandbox')).toBe('allow-scripts')
await expect.poll(() => html.locator('#result').innerText()).toBe('INLINE_OK')
await successShot(page, 'html')
diff --git a/apps/web/tests/expected/plugin-config/official.expected.md b/apps/web/tests/expected/plugin-config/official.expected.md
index 5d0747dad9..c1cac31c15 100644
--- a/apps/web/tests/expected/plugin-config/official.expected.md
+++ b/apps/web/tests/expected/plugin-config/official.expected.md
@@ -26,8 +26,11 @@
- button "查看 网页搜索": 网页搜索
- text: 设置 DeepSeek 的搜索提供方。
- heading "已安装" [level=3]
-- text: "1"
+- text: "2"
- list:
- listitem:
- button "查看 @fixture/live-client": "@fixture/live-client"
- switch "启用 @fixture/live-client"
+ - listitem:
+ - button "查看 dsh-web-scaffold-defaults": dsh-web-scaffold-defaults
+ - switch "启用 dsh-web-scaffold-defaults" [checked]
diff --git a/apps/web/tests/expected/plugin-manager/live-enabled.expected.md b/apps/web/tests/expected/plugin-manager/live-enabled.expected.md
index 3a69e8b9b4..c9b222597d 100644
--- a/apps/web/tests/expected/plugin-manager/live-enabled.expected.md
+++ b/apps/web/tests/expected/plugin-manager/live-enabled.expected.md
@@ -26,9 +26,12 @@
- button "查看 网页搜索": 网页搜索
- text: 设置 DeepSeek 的搜索提供方。
- heading "已安装" [level=3]
-- text: "1"
+- text: "2"
- list:
- listitem:
- button "查看 @fixture/bundle": "@fixture/bundle"
- text: Registry description for the fixture bundle.
- switch "启用 @fixture/bundle" [checked]
+ - listitem:
+ - button "查看 dsh-web-scaffold-defaults": dsh-web-scaffold-defaults
+ - switch "启用 dsh-web-scaffold-defaults" [checked]
diff --git a/apps/web/tests/expected/plugin-manager/manager.expected.md b/apps/web/tests/expected/plugin-manager/manager.expected.md
index 8bdb57c4d1..bb3c6cce17 100644
--- a/apps/web/tests/expected/plugin-manager/manager.expected.md
+++ b/apps/web/tests/expected/plugin-manager/manager.expected.md
@@ -26,9 +26,12 @@
- button "查看 网页搜索": 网页搜索
- text: 设置 DeepSeek 的搜索提供方。
- heading "已安装" [level=3]
-- text: "1"
+- text: "2"
- list:
- listitem:
- button "查看 @fixture/bundle": "@fixture/bundle"
- text: Registry description for the fixture bundle.
- switch "启用 @fixture/bundle"
+ - listitem:
+ - button "查看 dsh-web-scaffold-defaults": dsh-web-scaffold-defaults
+ - switch "启用 dsh-web-scaffold-defaults" [checked]
diff --git a/apps/web/tests/message-actions.e2e.ts b/apps/web/tests/message-actions.e2e.ts
index a4f07bb745..74f59c0dd8 100644
--- a/apps/web/tests/message-actions.e2e.ts
+++ b/apps/web/tests/message-actions.e2e.ts
@@ -326,7 +326,7 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
const row = dialog.getByText('Performance & usage', { exact: true }).locator('../..')
await row.getByRole('button', { name: 'Detailed', exact: true }).click()
await page.getByRole('menuitem', { name: 'Compact', exact: true }).click()
- await expect.poll(() => scaffold.ctx.settings.get('ui-chat')).toMatchObject({ performanceUsage: 'compact' })
+ await expect.poll(() => scaffold.ctx.settings.describe().find(row => row.ns === 'ui-chat')?.value).toMatchObject({ performanceUsage: 'compact' })
await dialog.getByRole('button', { name: 'Close', exact: true }).click()
await expect.poll(() => stats.locator('button').count()).toBe(0)
expect(await stats.textContent()).not.toContain('turns')
@@ -341,7 +341,7 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
acknowledgeReloadConnectionLoss(tripwire, warningStart)
await row.getByRole('button', { name: 'Compact', exact: true }).click()
await page.getByRole('menuitem', { name: 'Detailed', exact: true }).click()
- await expect.poll(() => scaffold.ctx.settings.get('ui-chat')).toMatchObject({ performanceUsage: 'detailed' })
+ await expect.poll(() => scaffold.ctx.settings.describe().find(row => row.ns === 'ui-chat')?.value).toMatchObject({ performanceUsage: 'detailed' })
await dialog.getByRole('button', { name: 'Close', exact: true }).click()
await expect.poll(() => stats.locator('button').count()).toBe(2)
expect(await page.locator('[data-turn-tail]').getByRole('button', { name: /Ran for/ }).count()).toBe(0)
diff --git a/apps/web/tests/models-settings-recovery.e2e.ts b/apps/web/tests/models-settings-recovery.e2e.ts
index 3e2d664b93..262c2d1f80 100644
--- a/apps/web/tests/models-settings-recovery.e2e.ts
+++ b/apps/web/tests/models-settings-recovery.e2e.ts
@@ -1,5 +1,5 @@
/** Stored catalog drift remains repairable through the assembled Models settings page. */
-import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
+import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
@@ -26,10 +26,11 @@ describe('web e2e: repairs a stored provider after catalog drift', () => {
beforeAll(async () => {
home = await mkdtemp(join(tmpdir(), 'dsh-models-recovery-'))
- await writeFile(join(home, 'settings.yaml'), [
- 'llm-pi-ai:', ' providers:', ' openrouter:', ' models:',
- ' - id: "111"', ' zai: {}', ' acme-gateway:',
- ' baseURL: https://gateway.example/v1', ' models:', ' - id: "custom-model"', '',
+ await mkdir(join(home, 'profiles', 'scaffold'), { recursive: true })
+ await writeFile(join(home, 'profiles', 'scaffold', 'cordis.patch.yml'), [
+ '- id: llm-pi-ai', ' config:', ' providers:', ' openrouter:', ' models:',
+ ' - id: "111"', ' zai: {}', ' acme-gateway:',
+ ' baseURL: https://gateway.example/v1', ' models:', ' - id: "custom-model"', '',
].join('\n'))
scaffold = await launchWebScaffold({ harnessHome: home })
browser = await chromium.launch()
@@ -71,7 +72,7 @@ describe('web e2e: repairs a stored provider after catalog drift', () => {
await dialog.getByLabel('提供商', { exact: true }).selectOption('minimax-cn')
await dialog.getByRole('button', { name: '保存', exact: true }).click()
await dialog.getByText('已保存 minimax-cn。', { exact: true }).waitFor()
- expect(await readFile(join(home, 'settings.yaml'), 'utf8')).toContain('minimax-cn: {}')
+ expect(await readFile(join(home, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')).toContain('minimax-cn: {}')
expect(await dialog.getByText(FAILURE, { exact: true }).count()).toBe(1)
})
@@ -80,15 +81,15 @@ describe('web e2e: repairs a stored provider after catalog drift', () => {
await dialog.getByRole('button', { name: '编辑 openrouter', exact: true }).click()
await dialog.getByText('自定义设置', { exact: true }).click()
await dialog.getByLabel('API 地址', { exact: true }).fill('https://gateway.example/v1')
- const before = await readFile(join(home, 'settings.yaml'), 'utf8')
+ const before = await readFile(join(home, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')
await dialog.getByRole('button', { name: '保存', exact: true }).click()
await expect.poll(() => dialog.getByText(FAILURE, { exact: true }).count()).toBe(2)
- expect(await readFile(join(home, 'settings.yaml'), 'utf8')).toBe(before)
+ expect(await readFile(join(home, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')).toBe(before)
await dialog.getByRole('button', { name: '删除模型 1', exact: true }).click()
await dialog.getByRole('button', { name: '保存', exact: true }).click()
await dialog.getByText('已保存 openrouter。', { exact: true }).waitFor()
expect(await dialog.getByText(FAILURE, { exact: true }).count()).toBe(0)
- const repaired = await readFile(join(home, 'settings.yaml'), 'utf8')
+ const repaired = await readFile(join(home, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')
expect(repaired).not.toContain('111')
expect(repaired).toContain('baseURL: https://gateway.example/v1')
expect(tripwire.pageErrors).toEqual([])
diff --git a/apps/web/tests/models-settings.e2e.ts b/apps/web/tests/models-settings.e2e.ts
index a46439dcbb..5651303126 100644
--- a/apps/web/tests/models-settings.e2e.ts
+++ b/apps/web/tests/models-settings.e2e.ts
@@ -129,7 +129,7 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
await dialog.getByText('已保存 minimax-cn。', { exact: true }).waitFor({ timeout: 10_000 })
expect(await dialog.getByRole('img', { name: 'API 密钥已配置' }).count()).toBe(0)
expect(await dialog.getByRole('img', { name: 'API 密钥缺失' }).count()).toBe(0)
- const document = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
+ const document = await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')
expect(document).toContain('minimax-cn: {}')
expect(document).not.toContain('MINIMAX_CN_API_KEY')
}, 60_000)
@@ -155,7 +155,7 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
await dialog.getByRole('button', { name: '编辑 minimax-cn' }).click()
await dialog.getByRole('textbox', { name: 'API 密钥', exact: true }).fill('sk-e2e-minimax')
await dialog.getByRole('button', { name: '保存', exact: true }).click()
- // The profile lands in settings.yaml with only the derived reference, the
+ // The profile lands in cordis.patch.yml with only the derived reference, the
// key value lands in the harness home's .credentials.yaml, the dormant route
// registers, and the topology frame invalidates the page into the row.
await expect.poll(
@@ -164,7 +164,7 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
).toBe(0)
await dialog.getByRole('img', { name: 'API 密钥已配置' }).waitFor({ timeout: 10_000 })
await dialog.getByText('已保存 minimax-cn。', { exact: true }).waitFor({ timeout: 10_000 })
- const document = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
+ const document = await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')
expect(document).toContain('minimax-cn:')
expect(document).toContain('apiKeyEnv: MINIMAX_CN_API_KEY')
expect(document).not.toContain('sk-e2e-minimax')
@@ -189,7 +189,7 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
// stored profile beside the reference.
await expect.poll(async () => dialog.getByLabel('API 地址').count(), { timeout: 10_000 }).toBe(0)
await dialog.getByText('已保存 minimax-cn。', { exact: true }).waitFor({ timeout: 10_000 })
- const document = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
+ const document = await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')
expect(document).toContain('baseURL: https://gateway.minimax.example/v1')
expect(document).toContain('apiKeyEnv: MINIMAX_CN_API_KEY')
const snapshot = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
@@ -270,7 +270,7 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
const row = dialog.getByText('Acme Gateway', { exact: true }).first()
await row.waitFor({ timeout: 10_000 })
- const document = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
+ const document = await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')
expect(document).toContain('acme-gateway:')
await expect(scaffold.ctx.llm.resolveModelInfo('acme-gateway', 'acme-large')).resolves.toMatchObject({
inputModalities: ['text', 'image'],
@@ -294,7 +294,7 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
await dialog.getByText('自定义设置').click()
// The create card asked this route for a name and a protocol because
// nothing can default them; the editor reaches the same two fields rather
- // than sending the user to settings.yaml for what only this route names.
+ // than sending the user to cordis.patch.yml for what only this route names.
const protocol = dialog.getByLabel('API 协议')
await protocol.waitFor({ timeout: 10_000 })
expect(await protocol.inputValue()).toBe('openai-completions')
@@ -319,7 +319,7 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
// The status line names the route as the refreshed directory reports it;
// the target captured when the card opened still carries the old name.
await dialog.getByText('已保存 Acme 网关 (acme-gateway)。', { exact: true }).waitFor({ timeout: 10_000 })
- const document = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
+ const document = await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')
expect(document).toContain('api: anthropic-messages')
expect(document).toContain('displayName: Acme 网关')
await expect(scaffold.ctx.llm.resolveModelInfo('acme-gateway', 'acme-large')).resolves.toMatchObject({
@@ -371,12 +371,12 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
const image = types.getByRole('checkbox', { name: '图片', exact: true })
await expect.poll(() => image.isChecked()).toBe(true)
expect(await types.getByRole('checkbox', { name: '文本', exact: true }).isChecked()).toBe(true)
- const before = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
+ const before = await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')
await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'catalog-inputs.expected.md'),
await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd), MODE)
await dialog.getByRole('button', { name: '保存', exact: true }).click()
await types.waitFor({ state: 'detached' })
- expect(await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')).toBe(before)
+ expect(await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')).toBe(before)
await edit.click()
await dialog.getByText('自定义设置').click()
@@ -414,7 +414,7 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
it('confirms an identified provider deletion before removing its profile and key', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-models-delete'))
- expect(await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')).not.toContain('openai:')
+ expect(await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')).not.toContain('openai:')
const settingsDialog = page.getByRole('dialog', { name: '设置' })
await settingsDialog.getByRole('button', { name: '删除 minimax-cn', exact: true }).click()
const deleteDialog = page.getByRole('dialog', { name: '删除 minimax-cn?' })
@@ -427,12 +427,12 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
await compareOrRefreshGolden(DELETE_EXPECTED, snapshot, MODE)
await deleteDialog.getByRole('button', { name: '取消', exact: true }).click()
- expect(await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')).toContain('minimax-cn:')
+ expect(await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')).toContain('minimax-cn:')
await settingsDialog.getByRole('button', { name: '删除 minimax-cn', exact: true }).click()
await page.getByRole('dialog', { name: '删除 minimax-cn?' })
.getByRole('button', { name: '删除 minimax-cn', exact: true }).click()
await expect.poll(
- async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'),
+ async () => readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8'),
{ timeout: 10_000 },
).not.toContain('minimax-cn:')
expect(await readFile(join(scaffold.harnessHome, '.credentials.yaml'), 'utf8'))
diff --git a/apps/web/tests/onboarding-deepseek-config.e2e.ts b/apps/web/tests/onboarding-deepseek-config.e2e.ts
index 4b4f6e0929..bf12ccbeb9 100644
--- a/apps/web/tests/onboarding-deepseek-config.e2e.ts
+++ b/apps/web/tests/onboarding-deepseek-config.e2e.ts
@@ -91,7 +91,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
expect((await page.locator('body').ariaSnapshot()).includes(secret)).toBe(false)
expect(browserConsole.some(line => line.includes(secret))).toBe(false)
- const acknowledgedSettings = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
+ const acknowledgedSettings = await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')
expect(acknowledgedSettings).toContain(`${WELCOME_NOTICE_ACK_FIELD}: ${WELCOME_NOTICE_VERSION}`)
// The ordinary Models surface reuses the refreshed join and exposes the
@@ -217,7 +217,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
await settings.getByRole('group', { name: '输入类型 1' }).getByRole('checkbox', { name: '图片' }).uncheck()
await settings.getByRole('button', { name: '保存', exact: true }).click()
await settings.getByLabel('模型 ID 1').waitFor({ state: 'detached', timeout: 15_000 })
- const savedDefaults = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
+ const savedDefaults = await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')
expect(savedDefaults).toContain('id: deepseek-flash')
expect(savedDefaults).toContain('inputModalities:')
expect(savedDefaults).toContain('- text')
@@ -252,7 +252,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
await settings.getByRole('button', { name: '保存', exact: true }).click()
await customModelId.waitFor({ state: 'detached', timeout: 15_000 })
- const document = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
+ const document = await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')
expect(document).toContain('id: private-preview')
expect(document).toContain('name: Private Preview')
expect(document).toContain('contextWindow: 131072')
diff --git a/apps/web/tests/onboarding-usable-provider.e2e.ts b/apps/web/tests/onboarding-usable-provider.e2e.ts
index 42a2593487..d17bd14a8b 100644
--- a/apps/web/tests/onboarding-usable-provider.e2e.ts
+++ b/apps/web/tests/onboarding-usable-provider.e2e.ts
@@ -93,7 +93,7 @@ describe.skipIf(MODE === 'record')('web e2e: another usable provider ends first-
await settings.getByText('已保存 minimax-cn。', { exact: true }).waitFor({ timeout: 15_000 })
// Only minimax-cn is reachable; DeepSeek still holds no credential.
- const document = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
+ const document = await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')
expect(document).toContain('apiKeyEnv: MINIMAX_CN_API_KEY')
const credentials = await readFile(join(scaffold.harnessHome, '.credentials.yaml'), 'utf8')
expect(credentials).toContain('MINIMAX_CN_API_KEY: sk-e2e-minimax')
diff --git a/apps/web/tests/plugin-config.e2e.ts b/apps/web/tests/plugin-config.e2e.ts
index 89ec1edb99..6e626ce463 100644
--- a/apps/web/tests/plugin-config.e2e.ts
+++ b/apps/web/tests/plugin-config.e2e.ts
@@ -1,6 +1,6 @@
// Web e2e scenario: the configuration pages on the Plugins page — the official
// pages a deployment's exposed host-plane namespaces produce, one field edited
-// through the real wire down to `$DSH_HOME/settings.yaml`, the override badge
+// through the real wire down to `$DSH_HOME/cordis.patch.yml`, the override badge
// and reset that layering produces, and a community bundle's row configuration
// registered by its own browser half. Zero model calls: everything is client
// state plus the settings document and the profile on a blank frame, so there
@@ -79,7 +79,7 @@ describe('web e2e: plugin configuration pages', () => {
/** The settings document as the Host has written it so far. */
async function settingsDocument(): Promise {
- return readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8').catch(() => '')
+ return readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8').catch(() => '')
}
it('lists one official page per exposed host-plane namespace after the official bundles', async () => {
@@ -182,7 +182,7 @@ describe('web e2e: plugin configuration pages', () => {
// The Save label returns only after both namespace controllers settle.
await expect.poll(() => save.isDisabled(), { timeout: 10_000 }).toBe(true)
const saved = await settingsDocument()
- expect(saved).toContain('subagent-model-selection:')
+ expect(saved).toContain('id: subagent-model-selection-settings')
expect(saved).toContain('maxDepth: 2')
expect(saved).toContain('enabled: true')
expect(saved).toContain('allowedModels:')
@@ -202,6 +202,8 @@ describe('web e2e: plugin configuration pages', () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-plugin-config-write'))
const panel = await openPlugins()
await openPage(panel, '终端')
+ const entry = [...scaffold.ctx.loader.entries()].find(row => row.options.id === 'bash-sandbox')!
+ const fiber = entry.fiber
const timeout = panel.getByLabel('命令超时(毫秒)')
await timeout.waitFor({ timeout: 10_000 })
@@ -219,6 +221,8 @@ describe('web e2e: plugin configuration pages', () => {
await expect.poll(async () => (await settingsDocument()).includes('timeoutMs: 12000'), { timeout: 10_000 })
.toBe(true)
+ expect(scaffold.ctx.shell.resolve({ command: 'true' }).timeoutMs).toBe(12000)
+ expect(entry.fiber === fiber).toBe(true)
// Presence in the user layer is what the badge reports, and the reset is
// offered only for a field that has one.
await expect.poll(() => panel.getByText('已覆盖').count(), { timeout: 5_000 }).toBe(1)
@@ -346,4 +350,6 @@ describe('web e2e: plugin configuration pages', () => {
expect(tripwire.warnings).toEqual([])
await assertFixtureInventory(SNAPSHOT_DIR, ['bundle.expected.md', 'official.expected.md', 'row.expected.md', 'subagent.expected.md'])
})
+
+
})
diff --git a/apps/web/tests/plugin-manager.e2e.ts b/apps/web/tests/plugin-manager.e2e.ts
index ca9fcc453f..e20b5b93ed 100644
--- a/apps/web/tests/plugin-manager.e2e.ts
+++ b/apps/web/tests/plugin-manager.e2e.ts
@@ -11,7 +11,7 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import { join } from 'node:path'
import {
- assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
+ SCAFFOLD_DEFAULTS_BUNDLE, assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, saveFailureShot } from './support.ts'
@@ -23,6 +23,8 @@ const EXPORTS_EXPECTED = join(SNAPSHOT_DIR, 'exports.expected.md')
const EXPORTS_EN_EXPECTED = join(SNAPSHOT_DIR, 'exports-en.expected.md')
const FIXTURE_PLUGINS = fileURLToPath(new URL('./fixtures/plugins', import.meta.url))
const MODE = webSnapshotMode()
+/** The profile manifest's bundles as the scaffold initializes them. */
+const SCAFFOLD_BUNDLES = ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', SCAFFOLD_DEFAULTS_BUNDLE]
describe('web e2e: plugin manager', () => {
let scaffold: WebScaffold
@@ -107,7 +109,7 @@ describe('web e2e: plugin manager', () => {
await toggle.click()
await expect.poll(async () => (JSON.parse(await homeFile('profiles', 'scaffold', 'package.json')) as {
dsh: { profile: { bundles: string[] } }
- }).dsh.profile.bundles, { timeout: 10_000 }).toEqual(['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'])
+ }).dsh.profile.bundles, { timeout: 10_000 }).toEqual(SCAFFOLD_BUNDLES)
await expect.poll(() => panel.getByText(/cannot resolve profile bundle/).count(), { timeout: 10_000 }).toBe(0)
expect((await scaffold.ctx.pluginManager.listBundles()).some(row => row.name === '@fixture/missing-bundle')).toBe(false)
expect(tripwire.pageErrors).toEqual([])
@@ -120,10 +122,10 @@ describe('web e2e: plugin manager', () => {
await panel.getByRole('button', { name: '查看 @fixture/bundle', exact: true }).waitFor({ timeout: 20_000 })
const toggle = panel.getByRole('switch', { name: '启用 @fixture/bundle' })
expect(await toggle.getAttribute('aria-checked')).toBe('false')
- // The profile's own group holds its one bundle; the installation's optional bundles open the Official
- // group, followed by the official plugins that registered their configuration, and its other bundles
- // stay off the page.
- expect(await panel.locator('[data-plugin-group="bundles"] [data-plugin-package]').count()).toBe(1)
+ // The profile's own group holds its fixture bundle and the scaffold's defaults bundle; the installation's
+ // optional bundles open the Official group, followed by the official plugins that registered their
+ // configuration, and its other bundles stay off the page.
+ expect(await panel.locator('[data-plugin-group="bundles"] [data-plugin-package]').count()).toBe(2)
expect(await panel.locator('[data-plugin-group="official"] [data-plugin-package]').count()).toBe(2)
expect(await panel.locator('[data-plugin-group="official"] [data-plugin-item]').count()).toBe(4)
expect(await panel.getByText('Beta', { exact: true }).count()).toBe(2)
@@ -283,9 +285,7 @@ describe('web e2e: plugin manager', () => {
const manifest = JSON.parse(await homeFile('profiles', 'scaffold', 'package.json')) as {
dsh: { profile: { bundles: string[] } }
}
- expect(manifest.dsh.profile.bundles).toEqual([
- '@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', '@deepseek-ai/dsh-experimental-agent-team-profile',
- ])
+ expect(manifest.dsh.profile.bundles).toEqual([...SCAFFOLD_BUNDLES, '@deepseek-ai/dsh-experimental-agent-team-profile'])
await panel.getByRole('button', { name: '查看 智能体团队', exact: true }).click()
for (const id of ['agent-team', 'tool-agent-team', 'ui-agent-team']) {
await panel.locator('[data-plugin-row]', { hasText: id }).first().waitFor()
@@ -347,7 +347,7 @@ describe('web e2e: plugin manager', () => {
const bundles = async () => (JSON.parse(await homeFile('profiles', 'scaffold', 'package.json')) as {
dsh: { profile: { bundles: string[] } }
}).dsh.profile.bundles
- await expect.poll(bundles, { timeout: 10_000 }).toEqual(['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', '@fixture/bundle'])
+ await expect.poll(bundles, { timeout: 10_000 }).toEqual([...SCAFFOLD_BUNDLES, '@fixture/bundle'])
// A live profile: the row mounts once the whole tree recomposed, the switch is on, and nothing waits for a restart.
await expect.poll(() => mounted()?.fiber?.state, { timeout: 20_000 }).toBe(2)
await expect.poll(() => toggle.getAttribute('aria-checked'), { timeout: 10_000 }).toBe('true')
@@ -408,7 +408,7 @@ describe('web e2e: startup-applied plugin management', () => {
expect(mounted()?.fiber?.state).toBeUndefined()
await toggle.click()
// The selection is saved and the switch turns on, but nothing mounts before the next start; a toast says so.
- await expect.poll(bundles).toEqual(['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', '@fixture/bundle'])
+ await expect.poll(bundles).toEqual([...SCAFFOLD_BUNDLES, '@fixture/bundle'])
await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('true')
await page.getByText('更改将在下次启动生效', { exact: true }).waitFor({ timeout: 10_000 })
expect(mounted()?.fiber?.state).toBeUndefined()
@@ -419,7 +419,7 @@ describe('web e2e: startup-applied plugin management', () => {
await panel.getByRole('button', { name: '返回插件列表' }).click()
await toggle.click()
- await expect.poll(bundles).toEqual(['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'])
+ await expect.poll(bundles).toEqual(SCAFFOLD_BUNDLES)
await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('false')
expect(tripwire.pageErrors).toEqual([])
} finally {
diff --git a/apps/web/tests/preview-boot.e2e.ts b/apps/web/tests/preview-boot.e2e.ts
index 270f19a181..3e92ed1e34 100644
--- a/apps/web/tests/preview-boot.e2e.ts
+++ b/apps/web/tests/preview-boot.e2e.ts
@@ -379,16 +379,22 @@ async function bootPreview(origin: string, browser: Browser): Promise {
// Settings and credentials both answer over the Remote carrier, so this
// half of the sweep posts the generated endpoints directly like the
// session read above.
- const settings = await remote<{ namespaces: { ns: string; revision: number }[] }>(
+ interface Setting { ns: string; revision: number; value: Record; user: Record }
+ const settings = await remote<{ writable: boolean; namespaces: Setting[] }>(
'settings/describe', {},
)
- const shell = settings.namespaces.find(namespace => namespace.ns === 'shell')
- if (shell === undefined) throw new Error('settings/describe omitted the shell namespace')
- await remote('settings/update', {
- ns: 'shell',
+ const shell = settings.namespaces.find(namespace => namespace.ns === 'bash-sandbox')
+ if (shell === undefined) throw new Error('settings/describe omitted the bash-sandbox namespace')
+ const saved = await remote('settings/update', {
+ ns: 'bash-sandbox',
patch: { timeoutMs: 61_000 },
expectedRevision: shell.revision,
})
+ const reread = await remote<{ namespaces: Setting[] }>('settings/describe', {})
+ const reset = await remote('settings/mutate', {
+ ns: 'bash-sandbox', ops: [{ op: 'unset', path: ['timeoutMs'] }], expectedRevision: saved.revision,
+ })
+ await remote('settings/update', { ns: 'ui-theme', patch: { fontSize: 17 } })
await remote('credentials/set', { ref: 'PREVIEW_TEST_SECRET', value: 'worker-only' })
const credentials = await remote>(
'credentials/describe',
@@ -397,6 +403,13 @@ async function bootPreview(origin: string, browser: Browser): Promise {
await remote('credentials/unset', { ref: 'PREVIEW_TEST_SECRET' })
await new Promise((resolve) => { setTimeout(resolve, 250) })
return {
+ settingsWritable: settings.writable,
+ settingIds: settings.namespaces.map(namespace => namespace.ns),
+ savedTimeout: saved.value.timeoutMs,
+ rereadTimeout: reread.namespaces.find(namespace => namespace.ns === 'bash-sandbox')?.value.timeoutMs,
+ resetTimeout: reset.value.timeoutMs,
+ initialTimeout: shell.value.timeoutMs,
+ resetOverrides: reset.user,
renamedTitle: secondRename.title,
renameAdvanced: secondRename.seq > firstRename.seq,
skillCount: skills.skills.length,
@@ -406,6 +419,16 @@ async function bootPreview(origin: string, browser: Browser): Promise {
seededSessionId: VFS_EXAMPLE_SESSION_IDS.main,
seededSessionTitle: SHOWCASE_TITLE,
})
+ expect(exercised.settingsWritable).toBe(true)
+ expect(exercised.settingIds).toEqual(expect.arrayContaining([
+ 'bash-sandbox', 'locale', 'ui-theme', 'ui-chat', 'ui-conversation', 'ui-settings', 'ui-settings-general',
+ ]))
+ expect(exercised.savedTimeout).toBe(61_000)
+ expect(exercised.rereadTimeout).toBe(61_000)
+ expect(exercised.resetTimeout).toBe(exercised.initialTimeout)
+ expect(exercised.resetOverrides).not.toHaveProperty('timeoutMs')
+ await expect.poll(() => page.evaluate(() => document.body.style.getPropertyValue('--dsh-content-font-size')))
+ .toBe('17px')
expect(exercised.renamedTitle).toBe(SHOWCASE_TITLE)
expect(exercised.renameAdvanced).toBe(true)
expect(exercised.skillCount).toBeGreaterThan(0)
diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts
index a91c45a1c6..173f25c7dc 100644
--- a/apps/web/tests/scaffold.ts
+++ b/apps/web/tests/scaffold.ts
@@ -33,8 +33,8 @@ import { expect } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import { DSH_LAUNCH_ENVIRONMENT_KEY, type LaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment'
import Loader from '@deepseek-ai/cordis-plugin-loader'
-import Include, { type PatchOptions } from '@deepseek-ai/cordis-plugin-include'
-import Group from '@deepseek-ai/cordis-plugin-group'
+import { entryListSchema, type PatchOptions } from '@deepseek-ai/cordis-plugin-include'
+import yaml from 'js-yaml'
import {
captureExpectedWorkspaceSnapshot,
captureWorkspaceSnapshot,
@@ -109,7 +109,9 @@ function appBoot(): AppBoot {
// WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_SETTINGS_NAMESPACE,
// WELCOME_NOTICE_VERSION, WELCOME_NOTICE_COPY,
// } from '@deepseek-ai/dsh-client-ui-settings-models'
-export const WELCOME_NOTICE_SETTINGS_NAMESPACE = 'ui-onboarding'
+export const WELCOME_NOTICE_SETTINGS_NAMESPACE = 'ui-settings-general'
+/** The installed bundle carrying the scaffold's deployment defaults; the plugin manager lists it beside fixture bundles. */
+export const SCAFFOLD_DEFAULTS_BUNDLE = 'dsh-web-scaffold-defaults'
export const WELCOME_NOTICE_ACK_FIELD = 'welcomeNoticeVersion'
export const WELCOME_NOTICE_VERSION = '2026-08-13.1'
export const WELCOME_NOTICE_COPY = {
@@ -641,7 +643,6 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise {
+ if (patch.id === undefined || !formEntries.has(patch.id) || patch.config === undefined) return patch
+ const config: unknown = patch.config
+ formDefaults.push({ id: patch.id, config })
+ const ordinary = { ...patch }
+ Reflect.deleteProperty(ordinary, 'config')
+ return ordinary
+ })
// Sessions inherit the gateway's process.cwd() default; run the boot from
// the temp workspace so tool cwd, session cwd, and fixtures agree.
@@ -730,13 +744,13 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise = {}
- const bundles = ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', ...options.profile.bundles ?? []]
- for (const entry of options.profile.packages) {
+ const bundles = ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', ...options.profile?.bundles ?? []]
+ for (const entry of options.profile?.packages ?? []) {
const manifest = JSON.parse(await readFile(join(entry.dir, 'package.json'), 'utf8')) as { name: string }
dependencies[manifest.name] = `file:${entry.dir}`
if (entry.enabled === true) bundles.push(manifest.name)
@@ -744,6 +758,13 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise layer.packageName),
- overlays: overlayPatches, telemetryDisabledEnv: undefined,
+ overlays: processOverlays, telemetryDisabledEnv: undefined,
}
// HMR gates file-driven reloads on application readiness, which the
// launcher commits after boot; this direct harness is ready at once.
@@ -775,26 +796,11 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise {
await page.getByRole('menuitem', { name: '仅可查看' }).click()
await dialog.getByRole('button', { name: '仅可查看' }).waitFor({ timeout: 10_000 })
- const document = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
- expect(document).toContain('permission:')
+ const document = await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')
+ expect(document).toContain('id: permission')
expect(document).toContain('defaultPreset: read-only')
expect(existing.snapshotEvents().find(event => event.type === 'permission/preset')?.data)
.toEqual({ preset: 'workspace-write' })
@@ -271,7 +271,7 @@ describe('web e2e: settings modal and General preferences', () => {
await confirmation.getByRole('checkbox').click()
await enable.click()
await dialog.getByRole('button', { name: '完全权限' }).waitFor({ timeout: 10_000 })
- const confirmedDocument = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
+ const confirmedDocument = await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')
expect(confirmedDocument).toContain('defaultPreset: danger-full-access')
const confirmed = scaffold.ctx.sessions.create(SessionId('settings-permission-confirmed'))
expect(confirmed.snapshotEvents().map(event => [event.type, event.data])).toEqual([
@@ -298,7 +298,8 @@ describe('web e2e: settings modal and General preferences', () => {
cube.click(),
])
expect(response.ok()).toBe(true)
- expect(await response.json()).toMatchObject({
+ const outcome: unknown = await response.json()
+ expect(outcome, JSON.stringify(outcome)).toMatchObject({
result: { ok: true, value: { ns: 'ui-theme', value: { preference } } },
})
}
@@ -311,8 +312,8 @@ describe('web e2e: settings modal and General preferences', () => {
const darkCube = initialDialog.getByRole('button', { name: '深色' })
await selectTheme(darkCube, 'dark')
await expect.poll(() => darkCube.getAttribute('aria-pressed'), { timeout: 5_000 }).toBe('true')
- await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
- .toMatch(/ui-theme:\n\s+preference: dark/)
+ await expect.poll(async () => readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8'), { timeout: 5_000 })
+ .toContain('preference: dark')
await page.keyboard.press('Escape')
// Hold the real application batch so the shell-owned loading page remains observable.
@@ -421,8 +422,8 @@ describe('web e2e: settings modal and General preferences', () => {
expect(dark.legacy).toBeNull()
expect(dark.token).not.toBe(light.token)
expectThemeColorSynchronized(dark)
- await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
- .toMatch(/ui-theme:\n\s+preference: dark/)
+ await expect.poll(async () => readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8'), { timeout: 5_000 })
+ .toContain('preference: dark')
await page.keyboard.press('Escape')
// Reload: the preference survives the background Host read + presenter update.
@@ -515,8 +516,8 @@ describe('web e2e: settings modal and General preferences', () => {
expect(await response.finished()).toBeNull()
const envelope = await response.json() as { result: { ok: boolean } }
expect(envelope.result.ok).toBe(true)
- await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
- .toMatch(new RegExp(`ui-theme:\n(?:\\s+\\w+: .*\n)*?\\s+fontSize: ${px}`))
+ await expect.poll(async () => readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8'), { timeout: 5_000 })
+ .toContain(`fontSize: ${px}`)
await page.getByRole('dialog', { name: '设置' }).getByText(String(px), { exact: true }).waitFor({ timeout: 5_000 })
await expect.poll(readFontSize, { timeout: 5_000 }).toBe(`${px}px`)
}
@@ -570,7 +571,7 @@ describe('web e2e: settings modal and General preferences', () => {
await details.getByRole('button', { name: '简洁', exact: true }).click()
await page.getByRole('menuitem', { name: label, exact: true }).click()
await details.getByRole('button', { name: label, exact: true }).waitFor({ timeout: 10_000 })
- await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
+ await expect.poll(async () => readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8'), { timeout: 5_000 })
.toContain(`transcriptView: ${mode}`)
await page.keyboard.press('Escape')
@@ -586,8 +587,8 @@ describe('web e2e: settings modal and General preferences', () => {
await restoredDetails.getByRole('button', { name: label, exact: true }).click()
await page.getByRole('menuitem', { name: '简洁', exact: true }).click()
await restoredDetails.getByRole('button', { name: '简洁', exact: true }).waitFor({ timeout: 10_000 })
- await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
- .toMatch(/ui-chat:\n\s+transcriptView: compact/)
+ await expect.poll(async () => readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8'), { timeout: 5_000 })
+ .toContain('transcriptView: compact')
await page.keyboard.press('Escape')
expect(tripwire.pageErrors).toEqual([])
}, 90_000)
@@ -601,8 +602,8 @@ describe('web e2e: settings modal and General preferences', () => {
await page.getByRole('menuitem', { name: '插话发送' }).click()
await dialog.getByRole('button', { name: '插话发送' }).waitFor({ timeout: 10_000 })
expect(await page.evaluate(() => localStorage.getItem('dsh.conversation.busyEnter'))).toBeNull()
- await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
- .toMatch(/ui-conversation:\n\s+busyEnter: steer/)
+ await expect.poll(async () => readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8'), { timeout: 5_000 })
+ .toContain('busyEnter: steer')
await page.keyboard.press('Escape')
const warningStart = tripwire.warnings.length
@@ -635,8 +636,8 @@ describe('web e2e: settings modal and General preferences', () => {
await page.getByRole('menuitem', { name: '排队发送' }).click()
await reloaded.getByRole('button', { name: '排队发送' }).waitFor({ timeout: 10_000 })
expect(await page.evaluate(() => localStorage.getItem('dsh.conversation.busyEnter'))).toBeNull()
- await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
- .toMatch(/ui-conversation:\n\s+busyEnter: queue/)
+ await expect.poll(async () => readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8'), { timeout: 5_000 })
+ .toContain('busyEnter: queue')
await page.keyboard.press('Escape')
expect(tripwire.pageErrors).toEqual([])
}, 90_000)
@@ -667,8 +668,8 @@ describe('web e2e: settings modal and General preferences', () => {
await enDialog.getByText(`Current version: ${version}`, { exact: true }).waitFor()
await expect.poll(() => enDialog.getByText('Appearance', { exact: true }).count(), { timeout: 5_000 }).toBe(1)
expect(await page.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull()
- await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
- .toMatch(/locale:\n\s+preference: en/)
+ await expect.poll(async () => readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8'), { timeout: 5_000 })
+ .toContain('preference: en')
// Reload keeps English; then restore zh so shared page state (and the
// other specs' 设置-anchored selectors + goldens) see the default again.
const warningStart = tripwire.warnings.length
@@ -703,8 +704,8 @@ describe('web e2e: settings modal and General preferences', () => {
await page.getByRole('menuitem', { name: '中文' }).click()
await page.getByRole('dialog', { name: '设置' }).waitFor({ timeout: 10_000 })
expect(await page.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull()
- await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
- .toMatch(/locale:\n\s+preference: zh/)
+ await expect.poll(async () => readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8'), { timeout: 5_000 })
+ .toContain('preference: zh')
await page.keyboard.press('Escape')
expect(tripwire.pageErrors).toEqual([])
}, 90_000)
@@ -737,7 +738,7 @@ describe('web e2e: settings modal and General preferences', () => {
await enPage.keyboard.press('Escape')
await expect.poll(() => enPage.getByRole('dialog', { name: 'Settings' }).count(), { timeout: 5_000 }).toBe(0)
await enPage.getByRole('navigation', { name: 'Global panels' }).getByRole('button', { name: 'Plugins', exact: true }).click()
- await enPage.getByText('This deployment runs without a manageable profile, so plugins cannot be installed or switched here.', { exact: true })
+ await enPage.getByRole('button', { name: 'Add plugin', exact: true })
.waitFor({ timeout: 10_000 })
// This page has no closing inventory spec to sweep its console, so the
// scenario clears both tripwire channels itself.
diff --git a/apps/web/tests/settings-import.e2e.ts b/apps/web/tests/settings-import.e2e.ts
new file mode 100644
index 0000000000..44a14f0ac0
--- /dev/null
+++ b/apps/web/tests/settings-import.e2e.ts
@@ -0,0 +1,44 @@
+// Web e2e scenario: a `settings.yaml` left in the harness home by earlier releases is imported into the
+// scaffold profile at start, through the shipped entries, and the imported values reach the page.
+import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { chromium } from 'playwright'
+import { expect, it } from 'vitest'
+import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
+import yaml from 'js-yaml'
+import { WELCOME_NOTICE_VERSION, launchWebScaffold, watchConsole } from './scaffold.ts'
+import { ZH_BROWSER_LOCALE } from './support.ts'
+
+it('imports settings.yaml into the profile once and applies the imported values', async () => {
+ const harnessHome = mkdtempSync(join(tmpdir(), 'dsh-settings-import-'))
+ writeFileSync(join(harnessHome, 'settings.yaml'), [
+ 'ui-theme:', ' fontSize: 16',
+ 'ui-developer-tools:', ' enabled: false',
+ 'ui-onboarding:', ` welcomeNoticeVersion: '${WELCOME_NOTICE_VERSION}'`,
+ '',
+ ].join('\n'))
+ const scaffold = await launchWebScaffold({ harnessHome, welcomeNoticePending: true })
+ const browser = await chromium.launch()
+ try {
+ const patchPath = join(harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml')
+ type Row = { id?: string; config?: Record }
+ const config = (id: string): Record | undefined =>
+ (yaml.load(readFileSync(patchPath, 'utf8'), { schema: entryListSchema }) as Row[]).find(row => row.id === id)?.config
+ await expect.poll(() => config('ui-theme')?.['fontSize'], { timeout: 10_000 }).toBe(16)
+ expect(config('ui-settings')?.['enabled']).toBe(false)
+ expect(config('ui-settings-general')?.['welcomeNoticeVersion']).toBe(WELCOME_NOTICE_VERSION)
+ expect(existsSync(join(harnessHome, 'settings.yaml'))).toBe(false)
+ expect(readFileSync(join(harnessHome, 'settings.yaml.imported'), 'utf8')).toContain('fontSize: 16')
+
+ const page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
+ const tripwire = watchConsole(page)
+ await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
+ await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
+ await expect.poll(() => page.evaluate(() => document.body.style.getPropertyValue('--dsh-content-font-size')), { timeout: 10_000 }).toBe('16px')
+ expect(tripwire.pageErrors).toEqual([])
+ } finally {
+ await browser.close()
+ await scaffold.close()
+ }
+}, 120_000)
diff --git a/apps/web/tests/turn-tail-actions.e2e.ts b/apps/web/tests/turn-tail-actions.e2e.ts
index f4f33e8f1d..2ed107815f 100644
--- a/apps/web/tests/turn-tail-actions.e2e.ts
+++ b/apps/web/tests/turn-tail-actions.e2e.ts
@@ -255,7 +255,7 @@ describe('web e2e: assistant IconActions wait for the turn to end', () => {
expect(await process.count()).toBe(1)
expect(await process.getAttribute('aria-expanded')).toBe('false')
expect(await tool.isVisible()).toBe(false)
- await expect.poll(async () => readFile(join(scaffold!.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
+ await expect.poll(async () => readFile(join(scaffold!.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8'), { timeout: 5_000 })
.toContain(`transcriptView: ${mode}`)
await page.getByRole('button', { name: 'Settings', exact: true }).click()
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
index 4c8bac18c8..9532402928 100644
--- a/apps/web/tsconfig.json
+++ b/apps/web/tsconfig.json
@@ -60,6 +60,7 @@
"tests/plugin-install-registry.e2e.ts",
"tests/client-plugin-live.e2e.ts",
"tests/settings-chrome.e2e.ts",
+ "tests/settings-import.e2e.ts",
"tests/desktop-updates.e2e.ts",
"tests/developer-tools-settings.e2e.ts",
"tests/models-settings.e2e.ts",
diff --git a/docs/capability-seams.i18n.yaml b/docs/capability-seams.i18n.yaml
index 6a71bb9da7..7be9b473ba 100644
--- a/docs/capability-seams.i18n.yaml
+++ b/docs/capability-seams.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/capability-seams.md
-capability-seams.md: 072cb47af420136a08ce182cfa341bb42fc068e0
-capability-seams.zh.md: 138fb3241a00cb2f6ab8853dd9cbf1440fe6d490
+capability-seams.md: 13890605db55379005a0842004bcdbfc64119ee8
+capability-seams.zh.md: ff30bb3455d7fefc239109b48adb5f8fa015289e
diff --git a/docs/capability-seams.md b/docs/capability-seams.md
index 072cb47af4..13890605db 100644
--- a/docs/capability-seams.md
+++ b/docs/capability-seams.md
@@ -94,9 +94,11 @@ flowchart LR
pkg_tool_bash["tool-bash"]
pkg_hooks_claude_code["hooks-claude-code"]
pkg_hooks_codex["hooks-codex"]
+ pkg_config_editor["config-editor"]
+ svc_configEditor["ctx.configEditor
Profile configuration edits"]
pkg_settings["settings"]
- svc_settings["ctx.settings
User-settings seam"]
- pkg_settings_file["settings-file"]
+ pkg_agent_default_model["agent-default-model"]
+ svc_settings["ctx.settings
Plugin configuration forms"]
pkg_tool_subagent["tool-subagent"]
svc_subagentModelSelection["ctx.subagentModelSelection
Subagent model-selection preference"]
pkg_credentials["credentials"]
@@ -161,7 +163,6 @@ flowchart LR
pkg_skill_office["skill-office"]
svc_agents["ctx.agents
Agent service"]
pkg_acp["acp"]
- pkg_agent_default_model["agent-default-model"]
svc_agentDefaultModel["ctx.agentDefaultModel
Default Agent model selection"]
pkg_headless["headless"]
svc_agentLoop["ctx.agentLoop
Concrete loop driver"]
@@ -294,6 +295,7 @@ flowchart LR
pkg_compaction_basic --> svc_compaction
pkg_compaction_tool_result_pruner --> svc_toolResultPruner
pkg_computer_use --> svc_computerUse
+ pkg_config_editor --> svc_configEditor
pkg_cordis_host_runner --> svc_cordisInspect
pkg_cordis_host_runner --> svc_dynamicCordisRunner
pkg_credentials --> svc_credentials
@@ -362,7 +364,6 @@ flowchart LR
pkg_session_title_all_prompts_llm --> svc_sessionTitle
pkg_session_title_first_prompt_llm --> svc_sessionTitle
pkg_settings --> svc_settings
- pkg_settings_file --> svc_settings
pkg_shell --> svc_shell
pkg_shell_env --> svc_shellEnv
pkg_skill --> svc_skills
@@ -429,6 +430,8 @@ flowchart LR
svc_compaction --> pkg_compaction_basic
svc_computerUse --> pkg_experimental_computer_use_cua_driver_mcp
svc_computerUse --> pkg_experimental_computer_use_cua_driver_native
+ svc_configEditor --> pkg_agent_default_model
+ svc_configEditor --> pkg_settings
svc_connection --> pkg_api_gateway
svc_connection --> pkg_host_frontend_static
svc_cordisInspect --> pkg_tool_cordis
@@ -492,8 +495,6 @@ flowchart LR
svc_sessions --> pkg_session_query_sqlite
svc_sessions --> pkg_subagent_in_process_driver
svc_settings --> pkg_api_settings_controller
- svc_settings --> pkg_llm_deepseek
- svc_settings --> pkg_llm_pi_ai
svc_shell --> pkg_hooks_claude_code
svc_shell --> pkg_hooks_codex
svc_shell --> pkg_tool_bash
@@ -585,7 +586,8 @@ flowchart LR
| `ctx.typert` | `core` | [`typert-registry`](../packages/typert/registry) | - | [`typert-loader`](../packages/typert/loader), [`api-gateway`](../packages/api/gateway) | - | Plugins register live zod contributions directly or through dsh-typert-loader; the API gateway consumes invocation descriptors and providers, while other runtime consumers query schemas and reflection metadata at their own edges. |
| `ctx.typertGateway` | `core` | [`api-gateway`](../packages/api/gateway) | - | - | - | Associates generated Remote descriptors with live Cordis services, resolves registered identities, and exposes unary calls through the shared Connection RPC carrier. |
| `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session/session-persistence) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | [`agent-loop`](../packages/core/agent-loop), [`tool-bash`](../packages/shell/tool-bash), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`message-feedback`](../packages/feedback/message-feedback) | - | The JSONL backend persists the SessionEvent vocabulary as one artifact per Session. |
-| `ctx.settings` | `seam` | [`settings`](../packages/settings/settings) | [`settings-file`](../packages/settings/settings-file) | [`api-settings-controller`](../packages/api/settings-controller), [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai) | - | Plugins register namespace schemas and resolve layered values; providers store the raw document. The LLM adapters register their entry config as the composition base under the user section; the settings controller serves redacted layered descriptors and writes the user layer. |
+| `ctx.configEditor` | `core` | [`config-editor`](../packages/boot/config-editor) | - | [`settings`](../packages/settings/settings), [`agent-default-model`](../packages/core/agent-default-model) | - | Persists profile config patches under the application file lock and HMR queue, then reconciles Loader entries. |
+| `ctx.settings` | `core` | [`settings`](../packages/settings/settings) | - | [`api-settings-controller`](../packages/api/settings-controller) | - | Forms project volatile Config fields from active profile entries and delegate validated edits to config-editor. Plugins consume their own Config references. |
| `ctx.subagentModelSelection` | `core` | [`tool-subagent`](../packages/subagent/tool-subagent) | - | [`tool-subagent`](../packages/subagent/tool-subagent) | - | Owns the default-off settings namespace that Agent-scoped delegation tools sample when composing a new top-level Session. |
| `ctx.credentials` | `seam` | [`credentials`](../packages/credentials/credentials) | [`credentials-local`](../packages/credentials/credentials-local) | [`api-settings-controller`](../packages/api/settings-controller), [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai) | - | Configuration carries references to secrets; providers own the values. Consumers resolve per operation, so a rotated credential reaches the very next request; the settings controller exposes value-free views and write-only storage. |
| `ctx.authorization` | `seam` | [`authorization`](../packages/credentials/authorization) | - | [`llm-pi-ai`](../packages/llm/llm-pi-ai) | - | Flows are registered by the plugin that knows how to obtain one credential and keyed by the record they write; the seam owns the conversation and the one-attempt-per-key lifecycle, never the protocol. |
@@ -610,7 +612,7 @@ flowchart LR
| `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session/session-projection-cache) | - | [`api-session-controller`](../packages/api/session-controller), [`session-query`](../packages/session-query/session-query), [`session-reference`](../packages/context/session-reference), [`subagent`](../packages/subagent/subagent) | - | Durably checkpoints projection unit states per session (throttled + turn/end/detach mandatory points) and serves the cold-read ladder: cache row + persistence tail replay, so listings never load full logs. |
| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`skill-badge`](../packages/skill/skill-badge), [`skill-filesystem`](../packages/skill/skill-filesystem), [`skill-office`](../packages/skill/skill-office) | [`tool-skill`](../packages/skill/tool-skill) | - | Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies. |
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | - | Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation. |
-| `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`api-session-controller`](../packages/api/session-controller), [`headless`](../packages/bundle/headless) | - | Layers the default ModelSelection through settings so direct and Host-backed Agent entry points share one state owner. |
+| `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`api-session-controller`](../packages/api/session-controller), [`headless`](../packages/bundle/headless) | - | Reads the default ModelSelection from volatile Config and saves selections through the profile editor. |
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`base`](../packages/bundle/base), [`sdk-minimal`](../packages/bundle/sdk-minimal) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. |
| `ctx.goals` | `core` | [`goal`](../packages/goal/goal) | - | - | - | Folds revisioned objective state from the session log and keeps live continuation activation process-local. |
| `ctx.ssh` | `core` | [`ssh`](../packages/ssh/ssh) | - | [`fs-ssh`](../packages/ssh/fs-ssh), [`subprocess-ssh`](../packages/ssh/subprocess-ssh), [`sandbox-ssh`](../packages/ssh/sandbox-ssh) | - | Owns one authenticated OpenSSH connection, installed helper identity, independent program streams and disconnect cleanup for the paired remote providers. |
diff --git a/docs/capability-seams.zh.md b/docs/capability-seams.zh.md
index 138fb3241a..ff30bb3455 100644
--- a/docs/capability-seams.zh.md
+++ b/docs/capability-seams.zh.md
@@ -96,9 +96,11 @@ flowchart LR
pkg_tool_bash["tool-bash"]
pkg_hooks_claude_code["hooks-claude-code"]
pkg_hooks_codex["hooks-codex"]
+ pkg_config_editor["config-editor"]
+ svc_configEditor["ctx.configEditor
Profile configuration edits"]
pkg_settings["settings"]
- svc_settings["ctx.settings
User-settings seam"]
- pkg_settings_file["settings-file"]
+ pkg_agent_default_model["agent-default-model"]
+ svc_settings["ctx.settings
Plugin configuration forms"]
pkg_tool_subagent["tool-subagent"]
svc_subagentModelSelection["ctx.subagentModelSelection
Subagent model-selection preference"]
pkg_credentials["credentials"]
@@ -163,7 +165,6 @@ flowchart LR
pkg_skill_office["skill-office"]
svc_agents["ctx.agents
Agent service"]
pkg_acp["acp"]
- pkg_agent_default_model["agent-default-model"]
svc_agentDefaultModel["ctx.agentDefaultModel
Default Agent model selection"]
pkg_headless["headless"]
svc_agentLoop["ctx.agentLoop
Concrete loop driver"]
@@ -296,6 +297,7 @@ flowchart LR
pkg_compaction_basic --> svc_compaction
pkg_compaction_tool_result_pruner --> svc_toolResultPruner
pkg_computer_use --> svc_computerUse
+ pkg_config_editor --> svc_configEditor
pkg_cordis_host_runner --> svc_cordisInspect
pkg_cordis_host_runner --> svc_dynamicCordisRunner
pkg_credentials --> svc_credentials
@@ -364,7 +366,6 @@ flowchart LR
pkg_session_title_all_prompts_llm --> svc_sessionTitle
pkg_session_title_first_prompt_llm --> svc_sessionTitle
pkg_settings --> svc_settings
- pkg_settings_file --> svc_settings
pkg_shell --> svc_shell
pkg_shell_env --> svc_shellEnv
pkg_skill --> svc_skills
@@ -431,6 +432,8 @@ flowchart LR
svc_compaction --> pkg_compaction_basic
svc_computerUse --> pkg_experimental_computer_use_cua_driver_mcp
svc_computerUse --> pkg_experimental_computer_use_cua_driver_native
+ svc_configEditor --> pkg_agent_default_model
+ svc_configEditor --> pkg_settings
svc_connection --> pkg_api_gateway
svc_connection --> pkg_host_frontend_static
svc_cordisInspect --> pkg_tool_cordis
@@ -494,8 +497,6 @@ flowchart LR
svc_sessions --> pkg_session_query_sqlite
svc_sessions --> pkg_subagent_in_process_driver
svc_settings --> pkg_api_settings_controller
- svc_settings --> pkg_llm_deepseek
- svc_settings --> pkg_llm_pi_ai
svc_shell --> pkg_hooks_claude_code
svc_shell --> pkg_hooks_codex
svc_shell --> pkg_tool_bash
@@ -587,7 +588,8 @@ flowchart LR
| `ctx.typert` | `core` | [`typert-registry`](../packages/typert/registry) | - | [`typert-loader`](../packages/typert/loader), [`api-gateway`](../packages/api/gateway) | - | 插件直接或通过 dsh-typert-loader 注册实时 zod 贡献;API 网关消费调用描述符和提供方,其他运行时消费方则在各自边界查询 schema 与反射元数据。 |
| `ctx.typertGateway` | `core` | [`api-gateway`](../packages/api/gateway) | - | - | - | 将生成的 Remote 描述符与实时 Cordis 服务关联,解析已注册的身份,并通过共享的 Connection RPC 载体提供一元调用。 |
| `ctx.sessionPersistence` | `seam` | [`session-persistence`](../packages/session/session-persistence) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | [`agent-loop`](../packages/core/agent-loop), [`tool-bash`](../packages/shell/tool-bash), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`message-feedback`](../packages/feedback/message-feedback) | - | JSONL backend 把 SessionEvent 词汇持久化为每个 Session 一份产物。 |
-| `ctx.settings` | `seam` | [`settings`](../packages/settings/settings) | [`settings-file`](../packages/settings/settings-file) | [`api-settings-controller`](../packages/api/settings-controller), [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai) | - | 插件注册命名空间 schema 并解析分层值;提供方存储原始文档。LLM(大语言模型)适配器在用户分区下将其入口配置注册为组合基础;settings controller 提供经过脱敏的分层描述符,并写入用户层。 |
+| `ctx.configEditor` | `core` | [`config-editor`](../packages/boot/config-editor) | - | [`settings`](../packages/settings/settings), [`agent-default-model`](../packages/core/agent-default-model) | - | Persists profile config patches under the application file lock and HMR queue, then reconciles Loader entries. |
+| `ctx.settings` | `core` | [`settings`](../packages/settings/settings) | - | [`api-settings-controller`](../packages/api/settings-controller) | - | Forms project volatile Config fields from active profile entries and delegate validated edits to config-editor. Plugins consume their own Config references. |
| `ctx.subagentModelSelection` | `core` | [`tool-subagent`](../packages/subagent/tool-subagent) | - | [`tool-subagent`](../packages/subagent/tool-subagent) | - | 拥有默认关闭的设置命名空间;Agent 作用域的委派工具会在组合新顶层 Session 时读取它。 |
| `ctx.credentials` | `seam` | [`credentials`](../packages/credentials/credentials) | [`credentials-local`](../packages/credentials/credentials-local) | [`api-settings-controller`](../packages/api/settings-controller), [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai) | - | 配置携带对机密信息的引用;提供方拥有实际值。消费方按操作解析,因此轮换后的凭据会在紧接着的下一次请求中生效;settings controller 提供不含实际值的视图和只写存储。 |
| `ctx.authorization` | `seam` | [`authorization`](../packages/credentials/authorization) | - | [`llm-pi-ai`](../packages/llm/llm-pi-ai) | - | flow 由知道如何取得某份凭据的插件注册,并以其写入的记录为键;seam 拥有这段对话与"每个键同时只跑一次尝试"的生命周期,而非协议本身。 |
@@ -612,7 +614,7 @@ flowchart LR
| `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session/session-projection-cache) | - | [`api-session-controller`](../packages/api/session-controller), [`session-query`](../packages/session-query/session-query), [`session-reference`](../packages/context/session-reference), [`subagent`](../packages/subagent/subagent) | - | 按会话持久保存投影单元状态的检查点(节流检查点,以及轮次/结束/分离时的必选检查点),并提供冷读取阶梯:缓存行加持久化尾部回放,因此列表读取永远不需要加载完整日志。 |
| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`skill-badge`](../packages/skill/skill-badge), [`skill-filesystem`](../packages/skill/skill-filesystem), [`skill-office`](../packages/skill/skill-office) | [`tool-skill`](../packages/skill/tool-skill) | - | 合并提供方的 skill(技能)目录;tool-skill 渲染会话前缀目录,并加载完整的 skill 正文。 |
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | - | 拥有实时 Agent 句柄、创建/恢复工厂 seam,以及进程本地的发起方传播。 |
-| `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`api-session-controller`](../packages/api/session-controller), [`headless`](../packages/bundle/headless) | - | 通过 settings 分层默认 `ModelSelection`,让直接入口与 Host 支撑的 Agent 入口共享同一个状态所有者。 |
+| `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`api-session-controller`](../packages/api/session-controller), [`headless`](../packages/bundle/headless) | - | Reads the default ModelSelection from volatile Config and saves selections through the profile editor. |
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`base`](../packages/bundle/base), [`sdk-minimal`](../packages/bundle/sdk-minimal) | - | 唯一的具体循环插件;扩展包依赖 dsh-agent 的事件和服务,而不依赖此包。 |
| `ctx.goals` | `core` | [`goal`](../packages/goal/goal) | - | - | - | 从会话日志折叠带修订版本的目标状态,并将实时延续激活保留在进程本地。 |
| `ctx.ssh` | `core` | [`ssh`](../packages/ssh/ssh) | - | [`fs-ssh`](../packages/ssh/fs-ssh), [`subprocess-ssh`](../packages/ssh/subprocess-ssh), [`sandbox-ssh`](../packages/ssh/sandbox-ssh) | - | 负责一条经过认证的 OpenSSH 连接、已安装辅助程序身份、独立程序流,以及配套远端提供方的断连清理。 |
diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml
index 4210a71a33..43f977e619 100644
--- a/docs/config-catalog.i18n.yaml
+++ b/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: 2225721f4adf3e938da7fa74a3b1cb2b159b0ed4
-config-catalog.zh.md: 21e95ea2a3bbbcf3f4667dd3f26fc10e6f607d7b
+config-catalog.md: c46008dd88e51dc079d72e6605d06176c26bdea1
+config-catalog.zh.md: 7376476771902e1bbcba3ae4d6374b1087e14a72
diff --git a/docs/config-catalog.md b/docs/config-catalog.md
index 2225721f4a..c46008dd88 100644
--- a/docs/config-catalog.md
+++ b/docs/config-catalog.md
@@ -38,16 +38,20 @@ Source: [`packages/acp/acp/src/index.ts:75`](../packages/acp/acp/src/index.ts)
## `@deepseek-ai/dsh-agent-default-model`
```ts config-catalog
-/** Composition entry for the default model selection. */
+/** Default model selection supplied by plugin configuration. */
export interface Config {
/** Registered provider route. */
- provider: string
+ provider: Volatile
/** Provider-owned model id. */
- model: string
+ model: Volatile
+ /** Adapter-owned reasoning effort; omission follows the provider default. */
+ reasoningEffort: Volatile
}
```
-Source: [`packages/core/agent-default-model/src/index.ts:41`](../packages/core/agent-default-model/src/index.ts)
+Depends on: `Volatile` (`@deepseek-ai/cordis`)
+
+Source: [`packages/core/agent-default-model/src/index.ts:24`](../packages/core/agent-default-model/src/index.ts)
@@ -94,7 +98,7 @@ export interface Config {
* Maximum parallel-safe calls in flight per agent step. `1` is serial;
* omission defaults to {@link DEFAULT_MAX_PARALLEL_TOOL_CALLS}.
*/
- maxParallelToolCalls?: number
+ maxParallelToolCalls: Volatile
/** Agents created or resumed at plugin startup. */
agents: (AgentOptions & {
/** Stable config label used in logs and as the fresh combined-id prefix. */
@@ -109,9 +113,9 @@ export interface Config {
}
```
-Depends on: [`AgentOptions`](subsystems/core.md) · [`SessionId`](subsystems/core.md)
+Depends on: [`AgentOptions`](subsystems/core.md) · [`SessionId`](subsystems/core.md) · `Volatile` (`@deepseek-ai/cosmokit`)
-Source: [`packages/core/agent-loop/src/index.ts:319`](../packages/core/agent-loop/src/index.ts)
+Source: [`packages/core/agent-loop/src/index.ts:292`](../packages/core/agent-loop/src/index.ts)
@@ -139,10 +143,16 @@ Requires: `loader` · `sessionProjections`
export interface Config {
/** Deployment default when the caller omits a preset. */
default: string
+ /** User-selected default while the chooser is shown; edited through Settings. */
+ selectedDefault: Volatile
+ /** Whether new-session surfaces expose preset selection and the saved default applies. */
+ modeSelectionEnabled: Volatile
}
```
-Source: [`packages/preset/agent-preset-registry/src/preset.ts:12`](../packages/preset/agent-preset-registry/src/preset.ts)
+Depends on: `Volatile` (`@deepseek-ai/cordis`)
+
+Source: [`packages/preset/agent-preset-registry/src/preset.ts:13`](../packages/preset/agent-preset-registry/src/preset.ts)
@@ -367,23 +377,25 @@ Source: [`packages/attachment/attachment-local/src/index.ts:61`](../packages/att
Requires: `subprocess`
```ts config-catalog
-/** Plugin config (all optional — `static Config` supplies the defaults). */
+/** Validated plugin configuration with live command budgets. */
export interface Config {
/** Default working directory for commands (default: process.cwd()). */
- cwd?: string
+ cwd: Volatile
/** Default foreground timeout in milliseconds. */
- timeoutMs?: number
+ timeoutMs: Volatile
/** Upper bound for per-call timeout overrides. */
- maxTimeoutMs?: number
+ maxTimeoutMs: Volatile
/** Per-stream in-memory output cap; overflow spills to a temp file. */
- maxOutputBytes?: number
+ maxOutputBytes: Volatile
/** Per-stream spill-file cap; larger streams retain only their in-memory tail. */
- maxSpillBytes?: number
+ maxSpillBytes: Volatile
/** Grace period for kill escalation and inherited pipes; at most `MAX_TIMER_DELAY_MS`. */
- graceMs?: number
+ graceMs: Volatile
}
```
+Depends on: `Volatile` (`@deepseek-ai/cordis`)
+
Source: [`packages/shell/bash-local/src/index.ts:41`](../packages/shell/bash-local/src/index.ts)
@@ -501,6 +513,27 @@ export interface Config {
Source: [`packages/client/ui-sidebar-documentpreview/src/config.ts:5`](../packages/client/ui-sidebar-documentpreview/src/config.ts)
+
+
+## `@deepseek-ai/dsh-client-ui-theme`
+
+```ts config-catalog
+/** Runtime preferences projected to the browser. */
+export interface Config {
+ /** Browser palette preference. */
+ preference: Volatile
+ /** Browser font size in pixels. */
+ fontSize: Volatile
+}
+
+/** Theme preference persisted by the product Appearance row. */
+export type ThemePreference = typeof THEME_PREFERENCES[number]
+```
+
+Depends on: `Volatile` (`@deepseek-ai/cordis`)
+
+Source: [`packages/client/ui-theme/src/index.ts:22`](../packages/client/ui-theme/src/index.ts)
+
## `@deepseek-ai/dsh-compaction-basic`
@@ -882,16 +915,18 @@ Source: [`packages/experimental/ptc-runtime-python/src/index.ts:42`](../packages
## `@deepseek-ai/dsh-experimental-speech-to-text`
```ts config-catalog
-/** Composition defaults resolved before a transcription starts. */
+/** Live selection read before a transcription starts; `configure()` writes it through the profile. */
export interface Config {
/** Registered provider selected when the caller omits an id. */
- defaultProvider: string
+ defaultProvider: Volatile
/** Provider language hint selected when the caller omits one. */
- language: string
+ language: Volatile
}
```
-Source: [`packages/experimental/speech-to-text/src/index.ts:18`](../packages/experimental/speech-to-text/src/index.ts)
+Depends on: `Volatile` (`@deepseek-ai/cordis`)
+
+Source: [`packages/experimental/speech-to-text/src/index.ts:20`](../packages/experimental/speech-to-text/src/index.ts)
@@ -1326,43 +1361,43 @@ Requires: `llm`
*/
export interface Config {
/** Credential reference (environment-variable name) resolved per request; defaults to `DEEPSEEK_API_KEY`. */
- apiKeyEnv?: string
+ apiKeyEnv: Volatile
/** Endpoint base; falls back to $DEEPSEEK_BASE_URL from a trusted environment layer, then the public API. */
- baseURL?: string
+ baseURL: Volatile
/** Deployment thinking policy; `disabled` limits every conversation request to `off`. */
- thinking?: 'enabled' | 'disabled'
+ thinking: Volatile<'enabled' | 'disabled' | undefined>
/** Default thinking effort (default `high`); `off` disables thinking per request. */
- reasoningEffort?: 'off' | 'low' | 'high' | 'max'
+ reasoningEffort: Volatile<'off' | 'low' | 'high' | 'max' | undefined>
/** Default per-request output cap (default 256,000); a model's own cap and explicit request values win. */
- maxTokens?: number
+ maxTokens: Volatile
/** Positive context capacity used when the selected model has no exact value (default 1,000,000). */
- defaultContextWindow?: number
+ defaultContextWindow: Volatile
/** Advisory models shown by discovery consumers; defaults to V41 Flash and V4 Pro. */
- models?: DeepSeekCatalogModel[]
+ models: Volatile
/** Maximum provider idle time while one stream read is outstanding (default five minutes). */
- streamIdleTimeoutMs?: number
+ streamIdleTimeoutMs: Volatile
/** Maximum accumulated file-referenced image bytes per chat request (default 128 MiB). */
- maxRequestFilesBytes?: number
+ maxRequestFilesBytes: Volatile
/** Maximum accumulated base64 image payload after Files API fallback (default 20 MiB). */
- maxInlineRequestImageBytes?: number
+ maxInlineRequestImageBytes: Volatile
/** Maximum number of represented images per chat request (default 600). */
- maxImagesPerRequest?: number
+ maxImagesPerRequest: Volatile
/** Raw-byte removal step after the request exceeds its file bound (default 64 MiB). */
- imageOffloadByteQuantum?: number
+ imageOffloadByteQuantum: Volatile
/** Base64-byte removal step after inline fallback exceeds its bound (default 10 MiB). */
- inlineImageOffloadByteQuantum?: number
+ inlineImageOffloadByteQuantum: Volatile
/** Image-count removal step after the request exceeds its count bound (default 20). */
- imageOffloadCountQuantum?: number
+ imageOffloadCountQuantum: Volatile
/** Maximum duration of one request-image Files API resolution (default one minute). */
- filesApiTimeoutMs?: number
+ filesApiTimeoutMs: Volatile
/** Explicit lifetime assigned to each uploaded image (default seven days). */
- fileExpiresAfterSeconds?: number
+ fileExpiresAfterSeconds: Volatile
/** Remaining lifetime below which an indexed file is replaced (default one hour). */
- fileRefreshMarginSeconds?: number
+ fileRefreshMarginSeconds: Volatile
/** Oldest harness-owned files deleted before one quota-recovery upload retry (default 100). */
- fileQuotaCleanupBatch?: number
+ fileQuotaCleanupBatch: Volatile
/** Provider-owned model-request retry policy; omission uses normal mode with five retries. */
- retryPolicy?: RetryPolicyConfig
+ retryPolicy: Volatile
}
/** One optional model entry advertised by the direct-fetch adapter. */
@@ -1396,9 +1431,9 @@ export interface DeepSeekCatalogModel {
}
```
-Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts)
+Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`)
-Source: [`packages/llm/llm-deepseek/src/config.ts:25`](../packages/llm/llm-deepseek/src/config.ts)
+Source: [`packages/llm/llm-deepseek/src/config.ts:28`](../packages/llm/llm-deepseek/src/config.ts)
@@ -1414,7 +1449,7 @@ export interface Config {
* the dormant settings-driven posture: the adapter mounts with no routes
* and registers them the moment a settings section supplies profiles.
*/
- providers?: Record
+ providers: Volatile>
}
/** Configuration for one pi-ai provider route; the `providers` dict key IS the route. */
@@ -1671,9 +1706,9 @@ export type PiAiThinkingFormat = NonNullable
```
-Depends on: `Api` (`@earendil-works/pi-ai`) · `CacheRetention` (`@earendil-works/pi-ai`) · `Model` (`@earendil-works/pi-ai`) · `ModelThinkingLevel` (`@earendil-works/pi-ai`) · `OpenAICompletionsCompat` (`@earendil-works/pi-ai`) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · `ThinkingBudgets` (`@earendil-works/pi-ai`) · `Transport` (`@earendil-works/pi-ai`)
+Depends on: `Api` (`@earendil-works/pi-ai`) · `CacheRetention` (`@earendil-works/pi-ai`) · `Model` (`@earendil-works/pi-ai`) · `ModelThinkingLevel` (`@earendil-works/pi-ai`) · `OpenAICompletionsCompat` (`@earendil-works/pi-ai`) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · `ThinkingBudgets` (`@earendil-works/pi-ai`) · `Transport` (`@earendil-works/pi-ai`) · `Volatile` (`@deepseek-ai/cordis`)
-Source: [`packages/llm/llm-pi-ai/src/config.ts:221`](../packages/llm/llm-pi-ai/src/config.ts)
+Source: [`packages/llm/llm-pi-ai/src/config.ts:222`](../packages/llm/llm-pi-ai/src/config.ts)
@@ -1967,12 +2002,12 @@ export interface Config {
* never). The names `custom` and `auto` are reserved for derived state and
* the Auto review integration respectively.
*/
- presets?: Record
+ presets: Record
/**
* Default for new sessions. When omitted, the preset matching the composed
* sandbox and approval defaults is used.
*/
- defaultPreset?: string
+ defaultPreset: Volatile
}
/** One preset's sandbox/approval bundle and optional client presentation. */
@@ -1988,9 +2023,9 @@ export interface PresetSpec {
}
```
-Depends on: [`ApprovalPolicy`](subsystems/approval.md) · [`SandboxMode`](subsystems/sandbox.md)
+Depends on: [`ApprovalPolicy`](subsystems/approval.md) · [`SandboxMode`](subsystems/sandbox.md) · `Volatile` (`@deepseek-ai/cordis`)
-Source: [`packages/interaction/permission-presets/src/index.ts:156`](../packages/interaction/permission-presets/src/index.ts)
+Source: [`packages/interaction/permission-presets/src/index.ts:155`](../packages/interaction/permission-presets/src/index.ts)
@@ -2126,30 +2161,32 @@ Source: [`packages/ptc-runtime/ptc-runtime-node/src/index.ts:26`](../packages/pt
Requires: `subprocess`
```ts config-catalog
-/** Plugin config (all optional — `static Config` supplies the defaults). */
+/** Validated plugin configuration with live command budgets. */
export interface Config {
/** Default working directory for commands (default: process.cwd()). */
- cwd?: string
+ cwd: Volatile
/** Default foreground timeout in milliseconds. */
- timeoutMs?: number
+ timeoutMs: Volatile
/** Upper bound for per-call timeout overrides. */
- maxTimeoutMs?: number
+ maxTimeoutMs: Volatile
/** Per-stream in-memory output cap; overflow spills to a temp file. */
- maxOutputBytes?: number
+ maxOutputBytes: Volatile
/** Per-stream spill-file cap; larger streams retain only their in-memory tail. */
- maxSpillBytes?: number
+ maxSpillBytes: Volatile
/** Grace period for kill escalation and inherited pipes; at most `MAX_TIMER_DELAY_MS`. */
- graceMs?: number
+ graceMs: Volatile
/**
* Explicit pwsh executable. When omitted, well-known Windows install
* locations and PATH entries are probed in order (PowerShell 7 install,
* PATH entries such as the Microsoft Store install, then Windows
* PowerShell 5.1), falling back to a bare `pwsh` resolved through PATH.
*/
- pwshPath?: string
+ pwshPath: Volatile
}
```
+Depends on: `Volatile` (`@deepseek-ai/cordis`)
+
Source: [`packages/shell/pwsh-local/src/index.ts:58`](../packages/shell/pwsh-local/src/index.ts)
@@ -2558,26 +2595,6 @@ Depends on: [`SessionTitleLlmConfig`](../packages/session/session-title-llm/src/
Source: [`packages/session/session-title-first-prompt-llm/src/index.ts:15`](../packages/session/session-title-first-prompt-llm/src/index.ts)
-
-
-## `@deepseek-ai/dsh-settings-file`
-
-```ts config-catalog
-/** Plugin config: file location and hot-reload behavior. */
-export interface Config {
- /** Settings document path; defaults to `settings.yaml` under the harness home. */
- path?: string
- /** Harness home used when `path` is omitted; defaults to `$DSH_HOME` or `~/.dsh`. */
- dshHome?: string
- /** Watch the document and hot-publish external edits; defaults to true. */
- watch?: boolean
- /** Watcher write-settle window in milliseconds; defaults to 100. */
- debounceMs?: number
-}
-```
-
-Source: [`packages/settings/settings-file/src/index.ts:22`](../packages/settings/settings-file/src/index.ts)
-
## `@deepseek-ai/dsh-shell-env`
@@ -2835,12 +2852,14 @@ Source: [`packages/storage/storage-sqlite/src/index.ts:24`](../packages/storage/
/** Host configuration for continuable subagent capacity. */
export interface Config {
/** Maximum live children sharing uninterrupted continuable parent links; defaults to 8. */
- maxActiveSubagents?: number
+ maxActiveSubagents: Volatile
/** Default delegation depth for tools without an explicit limit; defaults to 1. */
- maxDepth?: number
+ maxDepth: Volatile
}
```
+Depends on: `Volatile` (`@deepseek-ai/cordis`)
+
Source: [`packages/subagent/subagent/src/index.ts:191`](../packages/subagent/subagent/src/index.ts)
@@ -3852,22 +3871,24 @@ Requires: `web`
/** Plugin config (all optional — `apply` fills env-var and constant defaults). */
export interface Config {
/** Literal DeepSeek API key; prefer {@link apiKeyEnv} so no secret enters configuration files. */
- apiKey?: string
+ apiKey: Volatile
/** Credential reference resolved for each search; defaults to `DEEPSEEK_API_KEY`. */
- apiKeyEnv?: string
+ apiKeyEnv: Volatile
/** Anthropic-compatible endpoint base; `/messages` is appended. */
- baseURL?: string
+ baseURL: Volatile
/** Anthropic-format model name. Defaults to `deepseek-v4-flash`. */
- model?: string
+ model: Volatile
/** `anthropic-version` header value. Defaults to `2023-06-01`. */
- apiVersion?: string
+ apiVersion: Volatile
/** Upper bound on generated tokens for the Messages request. Defaults to 4096. */
- maxTokens?: number
+ maxTokens: Volatile
/** Maximum `web_search` server-tool uses per request. Defaults to 5. */
- maxUses?: number
+ maxUses: Volatile
}
```
+Depends on: `Volatile` (`@deepseek-ai/cordis`)
+
Source: [`packages/web/web-search-deepseek/src/index.ts:46`](../packages/web/web-search-deepseek/src/index.ts)
@@ -4045,7 +4066,6 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
- `@deepseek-ai/dsh-client-ui-sidebar-terminal` ([`packages/client/ui-sidebar-terminal/src/index.ts`](../packages/client/ui-sidebar-terminal/src/index.ts))
- `@deepseek-ai/dsh-client-ui-skill` ([`packages/client/ui-skill/src/index.ts`](../packages/client/ui-skill/src/index.ts))
- `@deepseek-ai/dsh-client-ui-subagent` ([`packages/client/ui-subagent/src/index.ts`](../packages/client/ui-subagent/src/index.ts))
-- `@deepseek-ai/dsh-client-ui-theme` ([`packages/client/ui-theme/src/index.ts`](../packages/client/ui-theme/src/index.ts))
- `@deepseek-ai/dsh-client-ui-tool` ([`packages/client/ui-tool/src/index.ts`](../packages/client/ui-tool/src/index.ts))
- `@deepseek-ai/dsh-client-ui-trajectory` ([`packages/client/ui-trajectory/src/index.ts`](../packages/client/ui-trajectory/src/index.ts))
- `@deepseek-ai/dsh-client-ui-user-questions` ([`packages/client/ui-user-questions/src/index.ts`](../packages/client/ui-user-questions/src/index.ts))
@@ -4057,6 +4077,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
- `@deepseek-ai/dsh-commands` ([`packages/interaction/commands/src/index.ts`](../packages/interaction/commands/src/index.ts))
- `@deepseek-ai/dsh-compaction-image-offload` — requires `agents` · `sessions` ([`packages/compaction/compaction-image-offload/src/index.ts`](../packages/compaction/compaction-image-offload/src/index.ts))
- `@deepseek-ai/dsh-computer-use` ([`packages/computer-use/computer-use/src/index.ts`](../packages/computer-use/computer-use/src/index.ts))
+- `@deepseek-ai/dsh-config-editor` — requires `loader` · `profileContext` ([`packages/boot/config-editor/src/index.ts`](../packages/boot/config-editor/src/index.ts))
- `@deepseek-ai/dsh-cordis-client-runner` ([`packages/extensions/cordis-client-runner/src/index.ts`](../packages/extensions/cordis-client-runner/src/index.ts))
- `@deepseek-ai/dsh-deepseek-llm-api-extensions` ([`packages/llm/deepseek-llm-api-extensions/src/index.ts`](../packages/llm/deepseek-llm-api-extensions/src/index.ts))
- `@deepseek-ai/dsh-experimental-auto-review` — requires `llm` · `permissionPresets` · `sessions` · `tools` ([`packages/experimental/auto-review/src/index.ts`](../packages/experimental/auto-review/src/index.ts))
@@ -4079,6 +4100,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
- `@deepseek-ai/dsh-session-projection` ([`packages/session/session-projection/src/index.ts`](../packages/session/session-projection/src/index.ts))
- `@deepseek-ai/dsh-session-stats` — requires `sessionProjections` ([`packages/session/session-stats/src/index.ts`](../packages/session/session-stats/src/index.ts))
- `@deepseek-ai/dsh-session-turn-outline` — requires `sessionProjections` ([`packages/session/session-turn-outline/src/index.ts`](../packages/session/session-turn-outline/src/index.ts))
+- `@deepseek-ai/dsh-settings` — requires `configEditor` · `profileContext` ([`packages/settings/settings/src/index.ts`](../packages/settings/settings/src/index.ts))
- `@deepseek-ai/dsh-skill-badge` — requires `skills` ([`packages/skill/skill-badge/src/index.ts`](../packages/skill/skill-badge/src/index.ts))
- `@deepseek-ai/dsh-storage` ([`packages/storage/storage/src/index.ts`](../packages/storage/storage/src/index.ts))
- `@deepseek-ai/dsh-subprocess-local` ([`packages/subprocess/subprocess-local/src/index.ts`](../packages/subprocess/subprocess-local/src/index.ts))
@@ -4107,7 +4129,6 @@ Abstract service classes — a deployment loads a concrete implementation packag
- `@deepseek-ai/dsh-sandbox` — abstract `SandboxProvider` ([`packages/sandbox/sandbox/src/index.ts`](../packages/sandbox/sandbox/src/index.ts))
- `@deepseek-ai/dsh-session-persistence` — abstract `SessionPersistence` ([`packages/session/session-persistence/src/index.ts`](../packages/session/session-persistence/src/index.ts))
- `@deepseek-ai/dsh-session-query` — abstract `SessionQueryEngine` ([`packages/session-query/session-query/src/index.ts`](../packages/session-query/session-query/src/index.ts))
-- `@deepseek-ai/dsh-settings` — abstract `SettingsProvider` ([`packages/settings/settings/src/index.ts`](../packages/settings/settings/src/index.ts))
- `@deepseek-ai/dsh-shell` — abstract `ShellExecutor` ([`packages/shell/shell/src/index.ts`](../packages/shell/shell/src/index.ts))
- `@deepseek-ai/dsh-spill` — abstract `SpillStore` ([`packages/spill/spill/src/index.ts`](../packages/spill/spill/src/index.ts))
- `@deepseek-ai/dsh-subprocess` — abstract `SubprocessRuntime` ([`packages/subprocess/subprocess/src/index.ts`](../packages/subprocess/subprocess/src/index.ts))
diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md
index 21e95ea2a3..7376476771 100644
--- a/docs/config-catalog.zh.md
+++ b/docs/config-catalog.zh.md
@@ -15,7 +15,7 @@
## `@deepseek-ai/dsh-acp`
-需要:`agents` · `llm` · `sessionPersistence` · `sessions`
+需要: `agents` · `llm` · `sessionPersistence` · `sessions`
```ts config-catalog
/** Plugin config: the provider/model selection used for each ACP-created agent. */
@@ -33,29 +33,33 @@ export interface AcpConfig {
Depends on: `Stream` (`@agentclientprotocol/sdk`)
-来源:[`packages/acp/acp/src/index.ts:75`](../packages/acp/acp/src/index.ts)
+来源: [`packages/acp/acp/src/index.ts:75`](../packages/acp/acp/src/index.ts)
## `@deepseek-ai/dsh-agent-default-model`
```ts config-catalog
-/** Composition entry for the default model selection. */
+/** Default model selection supplied by plugin configuration. */
export interface Config {
/** Registered provider route. */
- provider: string
+ provider: Volatile
/** Provider-owned model id. */
- model: string
+ model: Volatile
+ /** Adapter-owned reasoning effort; omission follows the provider default. */
+ reasoningEffort: Volatile
}
```
-来源:[`packages/core/agent-default-model/src/index.ts:41`](../packages/core/agent-default-model/src/index.ts)
+Depends on: `Volatile` (`@deepseek-ai/cordis`)
+
+来源: [`packages/core/agent-default-model/src/index.ts:24`](../packages/core/agent-default-model/src/index.ts)
## `@deepseek-ai/dsh-agent-instructions`
-需要:`sessionProjections`
+需要: `sessionProjections`
```ts config-catalog
/** User-facing workspace instruction loader configuration. */
@@ -81,13 +85,13 @@ export interface Config {
}
```
-来源:[`packages/context/agent-instructions/src/config.ts:18`](../packages/context/agent-instructions/src/config.ts)
+来源: [`packages/context/agent-instructions/src/config.ts:18`](../packages/context/agent-instructions/src/config.ts)
## `@deepseek-ai/dsh-agent-loop`
-需要:`agents` · `sessions` · `llm` · `tools` · `systemPrompt` · `sessionProjections`
+需要: `agents` · `sessions` · `llm` · `tools` · `systemPrompt` · `sessionProjections`
```ts config-catalog
/** Agent-loop plugin configuration. */
@@ -96,7 +100,7 @@ export interface Config {
* Maximum parallel-safe calls in flight per agent step. `1` is serial;
* omission defaults to {@link DEFAULT_MAX_PARALLEL_TOOL_CALLS}.
*/
- maxParallelToolCalls?: number
+ maxParallelToolCalls: Volatile
/** Agents created or resumed at plugin startup. */
agents: (AgentOptions & {
/** Stable config label used in logs and as the fresh combined-id prefix. */
@@ -111,9 +115,9 @@ export interface Config {
}
```
-Depends on: [`AgentOptions`](subsystems/core.zh.md) · [`SessionId`](subsystems/core.zh.md)
+Depends on: [`AgentOptions`](subsystems/core.zh.md) · [`SessionId`](subsystems/core.zh.md) · `Volatile` (`@deepseek-ai/cosmokit`)
-来源:[`packages/core/agent-loop/src/index.ts:319`](../packages/core/agent-loop/src/index.ts)
+来源: [`packages/core/agent-loop/src/index.ts:292`](../packages/core/agent-loop/src/index.ts)
@@ -141,16 +145,22 @@ export type Config = PresetDefinition
export interface Config {
/** Deployment default when the caller omits a preset. */
default: string
+ /** User-selected default while the chooser is shown; edited through Settings. */
+ selectedDefault: Volatile
+ /** Whether new-session surfaces expose preset selection and the saved default applies. */
+ modeSelectionEnabled: Volatile
}
```
-来源: [`packages/preset/agent-preset-registry/src/preset.ts:12`](../packages/preset/agent-preset-registry/src/preset.ts)
+Depends on: `Volatile` (`@deepseek-ai/cordis`)
+
+来源: [`packages/preset/agent-preset-registry/src/preset.ts:13`](../packages/preset/agent-preset-registry/src/preset.ts)
## `@deepseek-ai/dsh-agent-tool-presentation`
-需要:`tools`
+需要: `tools`
```ts config-catalog
/** Plugin config. */
@@ -168,13 +178,13 @@ export interface Config {
Depends on: [`ToolPresentationMode`](subsystems/tools.zh.md)
-来源:[`packages/core/agent-tool-presentation/src/index.ts:38`](../packages/core/agent-tool-presentation/src/index.ts)
+来源: [`packages/core/agent-tool-presentation/src/index.ts:38`](../packages/core/agent-tool-presentation/src/index.ts)
## `@deepseek-ai/dsh-api-gateway`
-需要:`typert`
+需要: `typert`
```ts config-catalog
/** Gateway transport configuration. */
@@ -204,15 +214,13 @@ export interface Config {
}
```
-来源:[`packages/api/job-controller/src/index.ts:35`](../packages/api/job-controller/src/index.ts)
-
-
+Source: [`packages/api/job-controller/src/index.ts:35`](../packages/api/job-controller/src/index.ts)
## `@deepseek-ai/dsh-api-session-controller`
-需要:`agentDefaultModel` · `agents` · `attachments` · `fileUploads` · `fs` · `llm` · `sessions` · `sessionProjections` · `sessionQuery` · `typert` · `workspaceRegistry`
+需要: `agentDefaultModel` · `agents` · `attachments` · `fileUploads` · `fs` · `llm` · `sessions` · `sessionProjections` · `sessionQuery` · `typert` · `workspaceRegistry`
```ts config-catalog
/** Session Controller deployment policy. */
@@ -222,7 +230,7 @@ export interface Config {
}
```
-来源:[`packages/api/session-controller/src/index.ts:78`](../packages/api/session-controller/src/index.ts)
+来源: [`packages/api/session-controller/src/index.ts:78`](../packages/api/session-controller/src/index.ts)
@@ -242,7 +250,7 @@ export interface SettingsControllerInternals {
## `@deepseek-ai/dsh-api-terminal-controller`
-Requires: `subprocess` · `sandboxPolicy` · `typert`
+需要: `subprocess` · `sandboxPolicy` · `typert`
```ts config-catalog
/** Deployment limits and an optional shell profile. */
@@ -305,7 +313,7 @@ export interface Config {
## `@deepseek-ai/dsh-api-workspace-files`
-Requires: `fs` · `sandboxPolicy` · `sessions` · `typert`
+需要: `fs` · `sandboxPolicy` · `sessions` · `typert`
```ts config-catalog
/** Deployment caps on one page or one listing. */
@@ -327,7 +335,7 @@ export interface Config {
}
```
-来源:[`packages/api/workspace-files/src/index.ts:70`](../packages/api/workspace-files/src/index.ts)
+来源: [`packages/api/workspace-files/src/index.ts:70`](../packages/api/workspace-files/src/index.ts)
@@ -362,39 +370,41 @@ export interface Config {
}
```
-来源:[`packages/attachment/attachment-local/src/index.ts:61`](../packages/attachment/attachment-local/src/index.ts)
+来源: [`packages/attachment/attachment-local/src/index.ts:61`](../packages/attachment/attachment-local/src/index.ts)
## `@deepseek-ai/dsh-bash-local`
-需要:`subprocess`
+需要: `subprocess`
```ts config-catalog
-/** Plugin config (all optional — `static Config` supplies the defaults). */
+/** Validated plugin configuration with live command budgets. */
export interface Config {
/** Default working directory for commands (default: process.cwd()). */
- cwd?: string
+ cwd: Volatile
/** Default foreground timeout in milliseconds. */
- timeoutMs?: number
+ timeoutMs: Volatile
/** Upper bound for per-call timeout overrides. */
- maxTimeoutMs?: number
+ maxTimeoutMs: Volatile
/** Per-stream in-memory output cap; overflow spills to a temp file. */
- maxOutputBytes?: number
+ maxOutputBytes: Volatile
/** Per-stream spill-file cap; larger streams retain only their in-memory tail. */
- maxSpillBytes?: number
+ maxSpillBytes: Volatile
/** Grace period for kill escalation and inherited pipes; at most `MAX_TIMER_DELAY_MS`. */
- graceMs?: number
+ graceMs: Volatile
}
```
-来源:[`packages/shell/bash-local/src/index.ts:41`](../packages/shell/bash-local/src/index.ts)
+Depends on: `Volatile` (`@deepseek-ai/cordis`)
+
+来源: [`packages/shell/bash-local/src/index.ts:41`](../packages/shell/bash-local/src/index.ts)
## `@deepseek-ai/dsh-bash-sandbox`
-需要:`subprocess` · `sandbox` · `sandboxPolicy`
+需要: `subprocess` · `sandbox` · `sandboxPolicy`
```ts config-catalog
/**
@@ -409,13 +419,13 @@ export type Config = LocalConfig
Depends on: [`LocalConfig`](#deepseek-aidsh-bash-local)
-来源:[`packages/shell/bash-sandbox/src/index.ts:36`](../packages/shell/bash-sandbox/src/index.ts)
+来源: [`packages/shell/bash-sandbox/src/index.ts:36`](../packages/shell/bash-sandbox/src/index.ts)
## `@deepseek-ai/dsh-client-connection`
-需要:`credentials`
+需要: `credentials`
```ts config-catalog
/** Browser authentication, request limits, and connection recovery configuration. */
@@ -461,7 +471,7 @@ export interface ConnectionRecoveryConfig {
## `@deepseek-ai/dsh-client-hmr`
-需要:`clientModules` · `webServer`
+需要: `clientModules` · `webServer`
```ts config-catalog
/** Plugin config, validated by the same-named schemastery schema. */
@@ -471,7 +481,7 @@ export interface Config {
}
```
-来源:[`packages/client/hmr/src/index.ts:30`](../packages/client/hmr/src/index.ts)
+来源: [`packages/client/hmr/src/index.ts:30`](../packages/client/hmr/src/index.ts)
@@ -503,13 +513,34 @@ export interface Config {
}
```
-来源:[`packages/client/ui-sidebar-documentpreview/src/config.ts:5`](../packages/client/ui-sidebar-documentpreview/src/config.ts)
+来源: [`packages/client/ui-sidebar-documentpreview/src/config.ts:5`](../packages/client/ui-sidebar-documentpreview/src/config.ts)
+
+
+
+## `@deepseek-ai/dsh-client-ui-theme`
+
+```ts config-catalog
+/** Runtime preferences projected to the browser. */
+export interface Config {
+ /** Browser palette preference. */
+ preference: Volatile
+ /** Browser font size in pixels. */
+ fontSize: Volatile
+}
+
+/** Theme preference persisted by the product Appearance row. */
+export type ThemePreference = typeof THEME_PREFERENCES[number]
+```
+
+Depends on: `Volatile` (`@deepseek-ai/cordis`)
+
+来源: [`packages/client/ui-theme/src/index.ts:22`](../packages/client/ui-theme/src/index.ts)
## `@deepseek-ai/dsh-compaction-basic`
-需要:`llm` · `tokenMeter` · `sessions`
+需要: `llm` · `tokenMeter` · `sessions`
```ts config-catalog
/** Basic compaction configuration with an optional exact-target policy table. */
@@ -551,13 +582,13 @@ export interface ModelCompactPolicyConfig extends CompactionPolicyConfig {
}
```
-来源:[`packages/compaction/compaction-basic/src/types.ts:40`](../packages/compaction/compaction-basic/src/types.ts)
+来源: [`packages/compaction/compaction-basic/src/types.ts:40`](../packages/compaction/compaction-basic/src/types.ts)
## `@deepseek-ai/dsh-compaction-tool-result-pruner`
-需要:`tokenMeter`
+需要: `tokenMeter`
```ts config-catalog
/** Character-budget policy for deterministic tool-result pruning. */
@@ -571,13 +602,13 @@ export interface ToolResultPruneConfig {
}
```
-来源:[`packages/compaction/compaction-tool-result-pruner/src/types.ts:5`](../packages/compaction/compaction-tool-result-pruner/src/types.ts)
+来源: [`packages/compaction/compaction-tool-result-pruner/src/types.ts:5`](../packages/compaction/compaction-tool-result-pruner/src/types.ts)
## `@deepseek-ai/dsh-cordis-host-runner`
-需要:`tools`
+需要: `tools`
```ts config-catalog
/** Runner configuration. */
@@ -587,7 +618,7 @@ export interface Config {
}
```
-来源:[`packages/extensions/cordis-host-runner/src/index.ts:94`](../packages/extensions/cordis-host-runner/src/index.ts)
+来源: [`packages/extensions/cordis-host-runner/src/index.ts:94`](../packages/extensions/cordis-host-runner/src/index.ts)
@@ -607,13 +638,13 @@ export interface Config {
}
```
-来源:[`packages/credentials/credentials-local/src/index.ts:64`](../packages/credentials/credentials-local/src/index.ts)
+来源: [`packages/credentials/credentials-local/src/index.ts:64`](../packages/credentials/credentials-local/src/index.ts)
## `@deepseek-ai/dsh-experimental-agent-team`
-需要:`agents` · `sessions` · `sessionPersistence` · `sessionProjections` · `subagents`
+需要: `agents` · `sessions` · `sessionPersistence` · `sessionProjections` · `subagents`
```ts config-catalog
/** Team-service deployment limits. */
@@ -631,7 +662,7 @@ export interface Config {
}
```
-来源:[`packages/experimental/agent-team/src/types.ts:130`](../packages/experimental/agent-team/src/types.ts)
+来源: [`packages/experimental/agent-team/src/types.ts:130`](../packages/experimental/agent-team/src/types.ts)
@@ -655,7 +686,7 @@ export interface Config {
## `@deepseek-ai/dsh-experimental-browser-use-chrome-devtools-mcp`
-需要:`browserUse` · `agents` · `tools` · `systemPrompt`
+需要: `browserUse` · `agents` · `tools` · `systemPrompt`
```ts config-catalog
/** Fixed Chromium launch or existing-browser attachment settings. */
@@ -664,13 +695,13 @@ export type Config = BrowserMcpConfig
Depends on: `BrowserMcpConfig` (`@deepseek-ai/dsh-experimental-browser-use-runtime/mcp`)
-来源:[`packages/experimental/browser-use-chrome-devtools-mcp/src/index.ts:14`](../packages/experimental/browser-use-chrome-devtools-mcp/src/index.ts)
+来源: [`packages/experimental/browser-use-chrome-devtools-mcp/src/index.ts:14`](../packages/experimental/browser-use-chrome-devtools-mcp/src/index.ts)
## `@deepseek-ai/dsh-experimental-browser-use-playwright-mcp`
-需要:`browserUse` · `agents` · `tools` · `systemPrompt`
+需要: `browserUse` · `agents` · `tools` · `systemPrompt`
```ts config-catalog
/** Fixed Chromium launch or existing-browser attachment settings. */
@@ -679,13 +710,13 @@ export type Config = BrowserMcpConfig
Depends on: `BrowserMcpConfig` (`@deepseek-ai/dsh-experimental-browser-use-runtime/mcp`)
-来源:[`packages/experimental/browser-use-playwright-mcp/src/index.ts:15`](../packages/experimental/browser-use-playwright-mcp/src/index.ts)
+来源: [`packages/experimental/browser-use-playwright-mcp/src/index.ts:15`](../packages/experimental/browser-use-playwright-mcp/src/index.ts)
## `@deepseek-ai/dsh-experimental-browser-use-stagehand-native`
-需要:`browserUse` · `agents` · `tools` · `systemPrompt`
+需要: `browserUse` · `agents` · `tools` · `systemPrompt`
```ts config-catalog
/** Profile-owned browser connection and independent Stagehand model credentials. */
@@ -721,13 +752,13 @@ export interface StagehandModelConfig {
Depends on: `ModelConfig` (`@browserbasehq/stagehand`)
-来源:[`packages/experimental/browser-use-stagehand-native/src/index.ts:28`](../packages/experimental/browser-use-stagehand-native/src/index.ts)
+来源: [`packages/experimental/browser-use-stagehand-native/src/index.ts:28`](../packages/experimental/browser-use-stagehand-native/src/index.ts)
## `@deepseek-ai/dsh-experimental-computer-use-cua-driver-mcp`
-需要:`computerUse` · `tools`
+需要: `computerUse` · `tools`
```ts config-catalog
/** Installed executable and MCP connection overrides. */
@@ -745,13 +776,13 @@ export interface Config {
Depends on: [`McpClient`](../packages/mcp/mcp-client/src/index.ts)
-来源:[`packages/experimental/computer-use-cua-driver-mcp/src/index.ts:20`](../packages/experimental/computer-use-cua-driver-mcp/src/index.ts)
+来源: [`packages/experimental/computer-use-cua-driver-mcp/src/index.ts:20`](../packages/experimental/computer-use-cua-driver-mcp/src/index.ts)
## `@deepseek-ai/dsh-experimental-inspector`
-需要:`webServer`
+需要: `webServer`
```ts config-catalog
/** Host plugin configuration. Fetch capture is enabled by default. */
@@ -813,7 +844,7 @@ export interface InspectorOptions {
}
```
-来源:[`packages/experimental/inspector/src/index.ts:66`](../packages/experimental/inspector/src/index.ts)
+来源: [`packages/experimental/inspector/src/index.ts:66`](../packages/experimental/inspector/src/index.ts)
@@ -879,23 +910,25 @@ export interface Config {
}
```
-来源:[`packages/experimental/ptc-runtime-python/src/index.ts:42`](../packages/experimental/ptc-runtime-python/src/index.ts)
+来源: [`packages/experimental/ptc-runtime-python/src/index.ts:42`](../packages/experimental/ptc-runtime-python/src/index.ts)
## `@deepseek-ai/dsh-experimental-speech-to-text`
```ts config-catalog
-/** Composition defaults resolved before a transcription starts. */
+/** Live selection read before a transcription starts; `configure()` writes it through the profile. */
export interface Config {
/** Registered provider selected when the caller omits an id. */
- defaultProvider: string
+ defaultProvider: Volatile
/** Provider language hint selected when the caller omits one. */
- language: string
+ language: Volatile
}
```
-来源: [`packages/experimental/speech-to-text/src/index.ts:18`](../packages/experimental/speech-to-text/src/index.ts)
+Depends on: `Volatile` (`@deepseek-ai/cordis`)
+
+来源: [`packages/experimental/speech-to-text/src/index.ts:20`](../packages/experimental/speech-to-text/src/index.ts)
@@ -955,7 +988,7 @@ export interface Config {
## `@deepseek-ai/dsh-experimental-tool-agent-team`
-需要:`agents` · `agentTeams` · `tools` · `systemPrompt`
+需要: `agents` · `agentTeams` · `tools` · `systemPrompt`
```ts config-catalog
/** Tool routing configuration. */
@@ -967,13 +1000,13 @@ export interface Config {
}
```
-来源:[`packages/experimental/tool-agent-team/src/index.ts:17`](../packages/experimental/tool-agent-team/src/index.ts)
+来源: [`packages/experimental/tool-agent-team/src/index.ts:17`](../packages/experimental/tool-agent-team/src/index.ts)
## `@deepseek-ai/dsh-file-reference-local`
-需要:`agents` · `sessionProjections`
+需要: `agents`
```ts config-catalog
/** Local file-reference discovery configuration. */
@@ -987,7 +1020,7 @@ export interface Config {
}
```
-来源:[`packages/context/file-reference-local/src/index.ts:34`](../packages/context/file-reference-local/src/index.ts)
+来源: [`packages/context/file-reference-local/src/index.ts:34`](../packages/context/file-reference-local/src/index.ts)
@@ -1006,13 +1039,13 @@ export interface Config {
}
```
-来源:[`packages/fs/fs-local/src/index.ts:45`](../packages/fs/fs-local/src/index.ts)
+来源: [`packages/fs/fs-local/src/index.ts:45`](../packages/fs/fs-local/src/index.ts)
## `@deepseek-ai/dsh-fs-sandbox`
-需要:`sandboxPolicy`
+需要: `sandboxPolicy`
```ts config-catalog
/**
@@ -1026,13 +1059,13 @@ export type Config = LocalConfig
Depends on: [`LocalConfig`](#deepseek-aidsh-fs-local)
-来源:[`packages/fs/fs-sandbox/src/index.ts:45`](../packages/fs/fs-sandbox/src/index.ts)
+来源: [`packages/fs/fs-sandbox/src/index.ts:45`](../packages/fs/fs-sandbox/src/index.ts)
## `@deepseek-ai/dsh-goal`
-需要:`agents` · `sessionProjections`
+需要: `agents` · `sessionProjections`
```ts config-catalog
/** Deployment defaults for goal creation. */
@@ -1042,13 +1075,13 @@ export interface Config {
}
```
-来源:[`packages/goal/goal/src/index.ts:172`](../packages/goal/goal/src/index.ts)
+来源: [`packages/goal/goal/src/index.ts:172`](../packages/goal/goal/src/index.ts)
## `@deepseek-ai/dsh-headless`
-需要:`agentDefaultModel` · `agents` · `sessions`
+需要: `agentDefaultModel` · `agents` · `sessions`
```ts config-catalog
/** Plugin config: the task and run options resolved from this app's injected provider service. */
@@ -1062,7 +1095,7 @@ export interface Config {
}
```
-来源:[`packages/bundle/headless/src/index.ts:42`](../packages/bundle/headless/src/index.ts)
+来源: [`packages/bundle/headless/src/index.ts:42`](../packages/bundle/headless/src/index.ts)
@@ -1090,7 +1123,7 @@ Depends on: `ChokidarOptions` (`chokidar`)
## `@deepseek-ai/dsh-hooks-claude-code`
-需要:`shell` · `sessionProjections`
+需要: `shell` · `sessionProjections`
```ts config-catalog
/** Plugin config: where the CC hook config lives + substitution roots. */
@@ -1122,13 +1155,13 @@ export interface Config {
}
```
-来源:[`packages/hooks/hooks-claude-code/src/index.ts:51`](../packages/hooks/hooks-claude-code/src/index.ts)
+来源: [`packages/hooks/hooks-claude-code/src/index.ts:51`](../packages/hooks/hooks-claude-code/src/index.ts)
## `@deepseek-ai/dsh-hooks-codex`
-需要:`shell` · `sessionProjections`
+需要: `shell` · `sessionProjections`
```ts config-catalog
/** Plugin config: where the Codex hooks.json lives + the model name for payloads. */
@@ -1149,7 +1182,7 @@ export interface Config {
}
```
-来源:[`packages/hooks/hooks-codex/src/index.ts:50`](../packages/hooks/hooks-codex/src/index.ts)
+来源: [`packages/hooks/hooks-codex/src/index.ts:50`](../packages/hooks/hooks-codex/src/index.ts)
@@ -1163,13 +1196,13 @@ export interface Config {
}
```
-来源:[`packages/host/directory-picker-browse/src/index.ts:181`](../packages/host/directory-picker-browse/src/index.ts)
+来源: [`packages/host/directory-picker-browse/src/index.ts:181`](../packages/host/directory-picker-browse/src/index.ts)
## `@deepseek-ai/dsh-host-frontend-static`
-需要:`webServer` · `connection`
+需要: `webServer` · `connection`
```ts config-catalog
/** Plugin config: the dist anchor. */
@@ -1179,13 +1212,13 @@ export interface Config {
}
```
-来源:[`packages/host/frontend-static/src/index.ts:30`](../packages/host/frontend-static/src/index.ts)
+来源: [`packages/host/frontend-static/src/index.ts:30`](../packages/host/frontend-static/src/index.ts)
## `@deepseek-ai/dsh-host-open-in-app`
-需要:`webServer` · `connection` · `subprocess`
+需要: `webServer` · `connection` · `subprocess`
```ts config-catalog
/** Open-in-app host configuration. */
@@ -1210,7 +1243,7 @@ export interface Config {
}
```
-来源:[`packages/host/open-in-app/src/index.ts:50`](../packages/host/open-in-app/src/index.ts)
+来源: [`packages/host/open-in-app/src/index.ts:50`](../packages/host/open-in-app/src/index.ts)
@@ -1266,7 +1299,7 @@ export interface Config {
}
```
-来源:[`packages/host/webserver/src/index.ts:59`](../packages/host/webserver/src/index.ts)
+来源: [`packages/host/webserver/src/index.ts:59`](../packages/host/webserver/src/index.ts)
@@ -1284,7 +1317,7 @@ export interface Config {
}
```
-来源:[`packages/runtime-diagnostics/invariants/src/index.ts:15`](../packages/runtime-diagnostics/invariants/src/index.ts)
+来源: [`packages/runtime-diagnostics/invariants/src/index.ts:15`](../packages/runtime-diagnostics/invariants/src/index.ts)
@@ -1311,7 +1344,7 @@ export interface Config {
}
```
-来源:[`packages/jobs/jobs-local/src/index.ts:45`](../packages/jobs/jobs-local/src/index.ts)
+来源: [`packages/jobs/jobs-local/src/index.ts:45`](../packages/jobs/jobs-local/src/index.ts)
@@ -1330,43 +1363,43 @@ export interface Config {
*/
export interface Config {
/** Credential reference (environment-variable name) resolved per request; defaults to `DEEPSEEK_API_KEY`. */
- apiKeyEnv?: string
+ apiKeyEnv: Volatile
/** Endpoint base; falls back to $DEEPSEEK_BASE_URL from a trusted environment layer, then the public API. */
- baseURL?: string
+ baseURL: Volatile
/** Deployment thinking policy; `disabled` limits every conversation request to `off`. */
- thinking?: 'enabled' | 'disabled'
+ thinking: Volatile<'enabled' | 'disabled' | undefined>
/** Default thinking effort (default `high`); `off` disables thinking per request. */
- reasoningEffort?: 'off' | 'low' | 'high' | 'max'
+ reasoningEffort: Volatile<'off' | 'low' | 'high' | 'max' | undefined>
/** Default per-request output cap (default 256,000); a model's own cap and explicit request values win. */
- maxTokens?: number
+ maxTokens: Volatile
/** Positive context capacity used when the selected model has no exact value (default 1,000,000). */
- defaultContextWindow?: number
+ defaultContextWindow: Volatile
/** Advisory models shown by discovery consumers; defaults to V41 Flash and V4 Pro. */
- models?: DeepSeekCatalogModel[]
+ models: Volatile
/** Maximum provider idle time while one stream read is outstanding (default five minutes). */
- streamIdleTimeoutMs?: number
+ streamIdleTimeoutMs: Volatile
/** Maximum accumulated file-referenced image bytes per chat request (default 128 MiB). */
- maxRequestFilesBytes?: number
+ maxRequestFilesBytes: Volatile
/** Maximum accumulated base64 image payload after Files API fallback (default 20 MiB). */
- maxInlineRequestImageBytes?: number
+ maxInlineRequestImageBytes: Volatile
/** Maximum number of represented images per chat request (default 600). */
- maxImagesPerRequest?: number
+ maxImagesPerRequest: Volatile
/** Raw-byte removal step after the request exceeds its file bound (default 64 MiB). */
- imageOffloadByteQuantum?: number
+ imageOffloadByteQuantum: Volatile
/** Base64-byte removal step after inline fallback exceeds its bound (default 10 MiB). */
- inlineImageOffloadByteQuantum?: number
+ inlineImageOffloadByteQuantum: Volatile
/** Image-count removal step after the request exceeds its count bound (default 20). */
- imageOffloadCountQuantum?: number
+ imageOffloadCountQuantum: Volatile
/** Maximum duration of one request-image Files API resolution (default one minute). */
- filesApiTimeoutMs?: number
+ filesApiTimeoutMs: Volatile
/** Explicit lifetime assigned to each uploaded image (default seven days). */
- fileExpiresAfterSeconds?: number
+ fileExpiresAfterSeconds: Volatile
/** Remaining lifetime below which an indexed file is replaced (default one hour). */
- fileRefreshMarginSeconds?: number
+ fileRefreshMarginSeconds: Volatile
/** Oldest harness-owned files deleted before one quota-recovery upload retry (default 100). */
- fileQuotaCleanupBatch?: number
+ fileQuotaCleanupBatch: Volatile
/** Provider-owned model-request retry policy; omission uses normal mode with five retries. */
- retryPolicy?: RetryPolicyConfig
+ retryPolicy: Volatile
}
/** One optional model entry advertised by the direct-fetch adapter. */
@@ -1400,15 +1433,15 @@ export interface DeepSeekCatalogModel {
}
```
-Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts)
+Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`)
-来源:[`packages/llm/llm-deepseek/src/config.ts:25`](../packages/llm/llm-deepseek/src/config.ts)
+来源: [`packages/llm/llm-deepseek/src/config.ts:28`](../packages/llm/llm-deepseek/src/config.ts)
## `@deepseek-ai/dsh-llm-pi-ai`
-需要:`llm`
+需要: `llm`
```ts config-catalog
/** Plugin configuration: the provider routes this instance owns. */
@@ -1418,7 +1451,7 @@ export interface Config {
* the dormant settings-driven posture: the adapter mounts with no routes
* and registers them the moment a settings section supplies profiles.
*/
- providers?: Record
+ providers: Volatile>
}
/** Configuration for one pi-ai provider route; the `providers` dict key IS the route. */
@@ -1675,15 +1708,15 @@ export type PiAiThinkingFormat = NonNullable
```
-Depends on: `Api` (`@earendil-works/pi-ai`) · `CacheRetention` (`@earendil-works/pi-ai`) · `Model` (`@earendil-works/pi-ai`) · `ModelThinkingLevel` (`@earendil-works/pi-ai`) · `OpenAICompletionsCompat` (`@earendil-works/pi-ai`) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · `ThinkingBudgets` (`@earendil-works/pi-ai`) · `Transport` (`@earendil-works/pi-ai`)
+Depends on: `Api` (`@earendil-works/pi-ai`) · `CacheRetention` (`@earendil-works/pi-ai`) · `Model` (`@earendil-works/pi-ai`) · `ModelThinkingLevel` (`@earendil-works/pi-ai`) · `OpenAICompletionsCompat` (`@earendil-works/pi-ai`) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · `ThinkingBudgets` (`@earendil-works/pi-ai`) · `Transport` (`@earendil-works/pi-ai`) · `Volatile` (`@deepseek-ai/cordis`)
-来源:[`packages/llm/llm-pi-ai/src/config.ts:221`](../packages/llm/llm-pi-ai/src/config.ts)
+来源: [`packages/llm/llm-pi-ai/src/config.ts:222`](../packages/llm/llm-pi-ai/src/config.ts)
## `@deepseek-ai/dsh-llm-replay`
-需要:`llm`
+需要: `llm`
```ts config-catalog
/** Plugin config: the {@link ReplayConfig} inputs, each defaulting to its `DSH_SNAPSHOT_*` env var in `apply`. */
@@ -1756,26 +1789,26 @@ export interface ReplayModelConfig {
Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts)
-来源:[`packages/test-support/llm-replay/src/index.ts:1123`](../packages/test-support/llm-replay/src/index.ts)
+来源: [`packages/test-support/llm-replay/src/index.ts:1123`](../packages/test-support/llm-replay/src/index.ts)
## `@deepseek-ai/dsh-llm-retry`
-需要:`agents` · `sessionProjections`
+需要: `agents` · `sessionProjections`
```ts config-catalog
/** This policy executor has no config; providers own `retryPolicy`. */
export type Config = Readonly>
```
-来源:[`packages/llm/llm-retry/src/index.ts:25`](../packages/llm/llm-retry/src/index.ts)
+来源: [`packages/llm/llm-retry/src/index.ts:25`](../packages/llm/llm-retry/src/index.ts)
## `@deepseek-ai/dsh-lsp-stdio`
-需要:`fs` · `lsp` · `subprocess`
+需要: `fs` · `lsp` · `subprocess`
```ts config-catalog
/** Plugin configuration: provider id → local language-server configuration. */
@@ -1811,13 +1844,13 @@ export interface LspLocalServerConfig {
}
```
-来源:[`packages/lsp/lsp-stdio/src/index.ts:82`](../packages/lsp/lsp-stdio/src/index.ts)
+来源: [`packages/lsp/lsp-stdio/src/index.ts:82`](../packages/lsp/lsp-stdio/src/index.ts)
## `@deepseek-ai/dsh-mcp-client`
-需要:`tools`
+需要: `tools`
```ts config-catalog
/** Configuration for one stdio or Streamable HTTP MCP server. */
@@ -1888,13 +1921,13 @@ export interface ReconnectConfig {
}
```
-来源:[`packages/mcp/mcp-client/src/index.ts:104`](../packages/mcp/mcp-client/src/index.ts)
+来源: [`packages/mcp/mcp-client/src/index.ts:104`](../packages/mcp/mcp-client/src/index.ts)
## `@deepseek-ai/dsh-message-feedback`
-需要:`sessionPersistence` · `sessions`
+需要: `sessionPersistence` · `sessions`
```ts config-catalog
/** Required deployment policy for optional notes. */
@@ -1904,7 +1937,7 @@ export interface Config {
}
```
-来源:[`packages/feedback/message-feedback/src/index.ts:40`](../packages/feedback/message-feedback/src/index.ts)
+来源: [`packages/feedback/message-feedback/src/index.ts:40`](../packages/feedback/message-feedback/src/index.ts)
@@ -1960,7 +1993,7 @@ export interface Config {
## `@deepseek-ai/dsh-permission-presets`
-需要:`shell` · `approval` · `sessions` · `sessionProjections`
+需要: `shell` · `approval` · `sessions` · `sessionProjections`
```ts config-catalog
/** The {@link PermissionPresetService} config: preset table and composition default. */
@@ -1971,12 +2004,12 @@ export interface Config {
* never). The names `custom` and `auto` are reserved for derived state and
* the Auto review integration respectively.
*/
- presets?: Record
+ presets: Record
/**
* Default for new sessions. When omitted, the preset matching the composed
* sandbox and approval defaults is used.
*/
- defaultPreset?: string
+ defaultPreset: Volatile
}
/** One preset's sandbox/approval bundle and optional client presentation. */
@@ -1992,15 +2025,15 @@ export interface PresetSpec {
}
```
-Depends on: [`ApprovalPolicy`](subsystems/approval.zh.md) · [`SandboxMode`](subsystems/sandbox.zh.md)
+Depends on: [`ApprovalPolicy`](subsystems/approval.zh.md) · [`SandboxMode`](subsystems/sandbox.zh.md) · `Volatile` (`@deepseek-ai/cordis`)
-来源:[`packages/interaction/permission-presets/src/index.ts:156`](../packages/interaction/permission-presets/src/index.ts)
+来源: [`packages/interaction/permission-presets/src/index.ts:155`](../packages/interaction/permission-presets/src/index.ts)
## `@deepseek-ai/dsh-persona`
-需要:`systemPrompt`
+需要: `systemPrompt`
```ts config-catalog
/** Plugin config: the persona text this composition contributes. */
@@ -2023,13 +2056,13 @@ export interface Config {
}
```
-来源:[`packages/preset/persona/src/index.ts:30`](../packages/preset/persona/src/index.ts)
+来源: [`packages/preset/persona/src/index.ts:30`](../packages/preset/persona/src/index.ts)
## `@deepseek-ai/dsh-plan-mode`
-需要:`tools` · `systemPrompt` · `sessionProjections`
+需要: `tools` · `systemPrompt` · `sessionProjections`
```ts config-catalog
/** Deployment-owned plan guidance. */
@@ -2039,13 +2072,13 @@ export interface PlanModeConfig {
}
```
-来源:[`packages/plan/plan-mode/src/index.ts:70`](../packages/plan/plan-mode/src/index.ts)
+来源: [`packages/plan/plan-mode/src/index.ts:70`](../packages/plan/plan-mode/src/index.ts)
## `@deepseek-ai/dsh-plugin-manager`
-依赖: `loader` · `profileContext`
+需要: `loader` · `profileContext`
```ts config-catalog
/** The pnpm executable, the registries asked, and the limits for package diagnostics and registry lookups. */
@@ -2075,7 +2108,7 @@ export interface Config {
## `@deepseek-ai/dsh-plugin-package-inventory-deepseek`
-需要:`agents` · `deepseekLlmApiExtensions` · `loader`
+需要: `agents` · `deepseekLlmApiExtensions` · `loader`
```ts config-catalog
/** Plugin-package request contribution configuration. */
@@ -2085,7 +2118,7 @@ export interface Config {
}
```
-来源:[`packages/llm/plugin-package-inventory-deepseek/src/index.ts:32`](../packages/llm/plugin-package-inventory-deepseek/src/index.ts)
+来源: [`packages/llm/plugin-package-inventory-deepseek/src/index.ts:32`](../packages/llm/plugin-package-inventory-deepseek/src/index.ts)
@@ -2127,40 +2160,42 @@ export interface LaunchConfig {
## `@deepseek-ai/dsh-pwsh-local`
-需要:`subprocess`
+需要: `subprocess`
```ts config-catalog
-/** Plugin config (all optional — `static Config` supplies the defaults). */
+/** Validated plugin configuration with live command budgets. */
export interface Config {
/** Default working directory for commands (default: process.cwd()). */
- cwd?: string
+ cwd: Volatile
/** Default foreground timeout in milliseconds. */
- timeoutMs?: number
+ timeoutMs: Volatile
/** Upper bound for per-call timeout overrides. */
- maxTimeoutMs?: number
+ maxTimeoutMs: Volatile
/** Per-stream in-memory output cap; overflow spills to a temp file. */
- maxOutputBytes?: number
+ maxOutputBytes: Volatile
/** Per-stream spill-file cap; larger streams retain only their in-memory tail. */
- maxSpillBytes?: number
+ maxSpillBytes: Volatile
/** Grace period for kill escalation and inherited pipes; at most `MAX_TIMER_DELAY_MS`. */
- graceMs?: number
+ graceMs: Volatile
/**
* Explicit pwsh executable. When omitted, well-known Windows install
* locations and PATH entries are probed in order (PowerShell 7 install,
* PATH entries such as the Microsoft Store install, then Windows
* PowerShell 5.1), falling back to a bare `pwsh` resolved through PATH.
*/
- pwshPath?: string
+ pwshPath: Volatile
}
```
-来源:[`packages/shell/pwsh-local/src/index.ts:58`](../packages/shell/pwsh-local/src/index.ts)
+Depends on: `Volatile` (`@deepseek-ai/cordis`)
+
+来源: [`packages/shell/pwsh-local/src/index.ts:58`](../packages/shell/pwsh-local/src/index.ts)
## `@deepseek-ai/dsh-pwsh-sandbox`
-需要:`subprocess` · `sandbox` · `sandboxPolicy`
+需要: `subprocess` · `sandbox` · `sandboxPolicy`
```ts config-catalog
/**
@@ -2176,7 +2211,7 @@ export type Config = LocalConfig
Depends on: [`LocalConfig`](#deepseek-aidsh-pwsh-local)
-来源:[`packages/shell/pwsh-sandbox/src/index.ts:40`](../packages/shell/pwsh-sandbox/src/index.ts)
+来源: [`packages/shell/pwsh-sandbox/src/index.ts:40`](../packages/shell/pwsh-sandbox/src/index.ts)
@@ -2210,7 +2245,7 @@ export interface Config {
}
```
-来源:[`packages/guard/repeat-tool-reminder/src/index.ts:35`](../packages/guard/repeat-tool-reminder/src/index.ts)
+来源: [`packages/guard/repeat-tool-reminder/src/index.ts:35`](../packages/guard/repeat-tool-reminder/src/index.ts)
@@ -2242,13 +2277,13 @@ export interface Config {
}
```
-来源:[`packages/sandbox/sandbox-local/src/index.ts:45`](../packages/sandbox/sandbox-local/src/index.ts)
+来源: [`packages/sandbox/sandbox-local/src/index.ts:45`](../packages/sandbox/sandbox-local/src/index.ts)
## `@deepseek-ai/dsh-sandbox-policy`
-需要:`sessionProjections`
+需要: `sessionProjections`
```ts config-catalog
/**
@@ -2271,13 +2306,13 @@ export interface Config {
Depends on: [`SandboxMode`](subsystems/sandbox.zh.md)
-来源:[`packages/sandbox/sandbox-policy/src/index.ts:71`](../packages/sandbox/sandbox-policy/src/index.ts)
+来源: [`packages/sandbox/sandbox-policy/src/index.ts:71`](../packages/sandbox/sandbox-policy/src/index.ts)
## `@deepseek-ai/dsh-sdk-app`
-需要:`cmdlineArgs`
+需要: `cmdlineArgs`
```ts config-catalog
/** SDK stdio startup configuration. */
@@ -2287,13 +2322,13 @@ export interface Config {
}
```
-来源:[`packages/bundle/sdk-app/src/index.ts:23`](../packages/bundle/sdk-app/src/index.ts)
+来源: [`packages/bundle/sdk-app/src/index.ts:23`](../packages/bundle/sdk-app/src/index.ts)
## `@deepseek-ai/dsh-sdk-jsonrpc-server`
-需要:`agents`
+需要: `agents`
```ts config-catalog
/** JSON-RPC deployment config plus runtime-only test hooks. */
@@ -2311,13 +2346,13 @@ export interface JsonRpcConfig {
Depends on: `Readable` (`node:stream`) · `Writable` (`node:stream`)
-来源:[`packages/sdk/server/src/index.ts:25`](../packages/sdk/server/src/index.ts)
+来源: [`packages/sdk/server/src/index.ts:25`](../packages/sdk/server/src/index.ts)
## `@deepseek-ai/dsh-session-log-deepseek`
-需要:`deepseekLlmApiExtensions` · `sessions`
+需要: `deepseekLlmApiExtensions` · `sessions`
```ts config-catalog
/** Session-log request contribution configuration. */
@@ -2327,13 +2362,13 @@ export interface Config {
}
```
-来源:[`packages/session/session-log-deepseek/src/index.ts:38`](../packages/session/session-log-deepseek/src/index.ts)
+来源: [`packages/session/session-log-deepseek/src/index.ts:38`](../packages/session/session-log-deepseek/src/index.ts)
## `@deepseek-ai/dsh-session-log-export`
-需要:`commands` · `connection`
+需要: `commands` · `connection`
```ts config-catalog
/** Session-log archive policy. */
@@ -2346,7 +2381,7 @@ export interface Config {
export type SessionLogCompressionLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
```
-来源:[`packages/session-query/session-log-export/src/index.ts:46`](../packages/session-query/session-log-export/src/index.ts)
+来源: [`packages/session-query/session-log-export/src/index.ts:46`](../packages/session-query/session-log-export/src/index.ts)
@@ -2371,13 +2406,13 @@ export interface Config {
export type JsonlCompression = 'zstd' | 'none'
```
-来源:[`packages/session/session-persistence-jsonl/src/index.ts:90`](../packages/session/session-persistence-jsonl/src/index.ts)
+来源: [`packages/session/session-persistence-jsonl/src/index.ts:90`](../packages/session/session-persistence-jsonl/src/index.ts)
## `@deepseek-ai/dsh-session-projection-cache`
-需要:`storageDomain` · `sessionProjections` · `sessions`
+需要: `storageDomain` · `sessionProjections` · `sessions`
```ts config-catalog
/**
@@ -2395,13 +2430,13 @@ export interface Config {
}
```
-来源:[`packages/session/session-projection-cache/src/index.ts:63`](../packages/session/session-projection-cache/src/index.ts)
+来源: [`packages/session/session-projection-cache/src/index.ts:75`](../packages/session/session-projection-cache/src/index.ts)
## `@deepseek-ai/dsh-session-query-sqlite`
-需要:`sessions`
+需要: `sessions`
```ts config-catalog
/** Combined session-query configuration backed by SQLite full-text search. */
@@ -2443,13 +2478,13 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
Depends on: [`SessionQueryConfig`](../packages/session-query/session-query/src/index.ts)
-来源:[`packages/session-query/session-query-sqlite/src/index.ts:92`](../packages/session-query/session-query-sqlite/src/index.ts)
+来源: [`packages/session-query/session-query-sqlite/src/index.ts:92`](../packages/session-query/session-query-sqlite/src/index.ts)
## `@deepseek-ai/dsh-session-reference`
-需要:`sessionQuery`
+需要: `sessionQuery`
```ts config-catalog
/** Session-reference service configuration. */
@@ -2465,13 +2500,13 @@ export interface Config {
}
```
-来源:[`packages/context/session-reference/src/config.ts:11`](../packages/context/session-reference/src/config.ts)
+来源: [`packages/context/session-reference/src/config.ts:11`](../packages/context/session-reference/src/config.ts)
## `@deepseek-ai/dsh-session-telemetry-otel`
-需要:`sessions`
+需要: `sessions`
```ts config-catalog
/**
@@ -2510,13 +2545,13 @@ export enum SessionTelemetryMode {
Depends on: `BatchLogRecordProcessorOptions` (`@opentelemetry/sdk-logs`) · `OTLPExporterNodeConfigBase` (`@opentelemetry/otlp-exporter-base`)
-来源:[`packages/session/session-telemetry-otel/src/index.ts:100`](../packages/session/session-telemetry-otel/src/index.ts)
+来源: [`packages/session/session-telemetry-otel/src/index.ts:100`](../packages/session/session-telemetry-otel/src/index.ts)
## `@deepseek-ai/dsh-session-title`
-需要:`sessions`
+需要: `sessions` · `sessionProjections`
```ts config-catalog
/** Required deterministic fallback and accepted-title limits. */
@@ -2530,13 +2565,13 @@ export interface Config {
}
```
-来源:[`packages/session/session-title/src/index.ts:56`](../packages/session/session-title/src/index.ts)
+来源: [`packages/session/session-title/src/index.ts:56`](../packages/session/session-title/src/index.ts)
## `@deepseek-ai/dsh-session-title-all-prompts-llm`
-需要:`sessionTitle` · `llm` · `sessions`
+需要: `sessionTitle` · `llm` · `sessions`
```ts config-catalog
/** Required LLM policy; this plugin adds no defaults. */
@@ -2545,13 +2580,13 @@ export type Config = SessionTitleLlmConfig
Depends on: [`SessionTitleLlmConfig`](../packages/session/session-title-llm/src/index.ts)
-来源:[`packages/session/session-title-all-prompts-llm/src/index.ts:15`](../packages/session/session-title-all-prompts-llm/src/index.ts)
+来源: [`packages/session/session-title-all-prompts-llm/src/index.ts:15`](../packages/session/session-title-all-prompts-llm/src/index.ts)
## `@deepseek-ai/dsh-session-title-first-prompt-llm`
-需要:`sessionTitle` · `llm` · `sessions`
+需要: `sessionTitle` · `llm` · `sessions`
```ts config-catalog
/** Required LLM policy; this plugin adds no defaults. */
@@ -2560,27 +2595,7 @@ export type Config = SessionTitleLlmConfig
Depends on: [`SessionTitleLlmConfig`](../packages/session/session-title-llm/src/index.ts)
-来源:[`packages/session/session-title-first-prompt-llm/src/index.ts:15`](../packages/session/session-title-first-prompt-llm/src/index.ts)
-
-
-
-## `@deepseek-ai/dsh-settings-file`
-
-```ts config-catalog
-/** Plugin config: file location and hot-reload behavior. */
-export interface Config {
- /** Settings document path; defaults to `settings.yaml` under the harness home. */
- path?: string
- /** Harness home used when `path` is omitted; defaults to `$DSH_HOME` or `~/.dsh`. */
- dshHome?: string
- /** Watch the document and hot-publish external edits; defaults to true. */
- watch?: boolean
- /** Watcher write-settle window in milliseconds; defaults to 100. */
- debounceMs?: number
-}
-```
-
-来源:[`packages/settings/settings-file/src/index.ts:22`](../packages/settings/settings-file/src/index.ts)
+来源: [`packages/session/session-title-first-prompt-llm/src/index.ts:15`](../packages/session/session-title-first-prompt-llm/src/index.ts)
@@ -2594,7 +2609,7 @@ export interface Config {
}
```
-来源:[`packages/shell/shell-env/src/index.ts:28`](../packages/shell/shell-env/src/index.ts)
+来源: [`packages/shell/shell-env/src/index.ts:28`](../packages/shell/shell-env/src/index.ts)
@@ -2608,13 +2623,13 @@ export interface Config {
}
```
-来源:[`packages/skill/skill/src/index.ts:278`](../packages/skill/skill/src/index.ts)
+来源: [`packages/skill/skill/src/index.ts:278`](../packages/skill/skill/src/index.ts)
## `@deepseek-ai/dsh-skill-filesystem`
-需要:`skills`
+需要: `skills`
```ts config-catalog
/** Local filesystem skill provider configuration. */
@@ -2646,13 +2661,13 @@ export interface Config {
}
```
-来源:[`packages/skill/skill-filesystem/src/index.ts:49`](../packages/skill/skill-filesystem/src/index.ts)
+来源: [`packages/skill/skill-filesystem/src/index.ts:49`](../packages/skill/skill-filesystem/src/index.ts)
## `@deepseek-ai/dsh-skill-office`
-依赖:`skills`
+需要: `skills`
```ts config-catalog
/** Office skill resource location. */
@@ -2662,7 +2677,7 @@ export interface Config {
}
```
-来源:[`packages/skill/skill-office/src/index.ts:15`](../packages/skill/skill-office/src/index.ts)
+来源: [`packages/skill/skill-office/src/index.ts:15`](../packages/skill/skill-office/src/index.ts)
@@ -2691,13 +2706,13 @@ export interface Config {
}
```
-来源:[`packages/spill/spill-local/src/index.ts:31`](../packages/spill/spill-local/src/index.ts)
+来源: [`packages/spill/spill-local/src/index.ts:31`](../packages/spill/spill-local/src/index.ts)
## `@deepseek-ai/dsh-spill-policy`
-需要:`tools` · `sessionProjections`
+需要: `tools`
```ts config-catalog
/** Plugin config. */
@@ -2711,7 +2726,7 @@ export interface Config {
}
```
-来源:[`packages/spill/spill-policy/src/index.ts:61`](../packages/spill/spill-policy/src/index.ts)
+来源: [`packages/spill/spill-policy/src/index.ts:61`](../packages/spill/spill-policy/src/index.ts)
@@ -2745,13 +2760,13 @@ export interface Config {
}
```
-来源:[`packages/ssh/ssh/src/index.ts:17`](../packages/ssh/ssh/src/index.ts)
+来源: [`packages/ssh/ssh/src/index.ts:17`](../packages/ssh/ssh/src/index.ts)
## `@deepseek-ai/dsh-storage-domain`
-需要:`storage`
+需要: `storage`
```ts config-catalog
/**
@@ -2768,13 +2783,13 @@ export interface Config {
}
```
-来源:[`packages/storage/storage-domain/src/index.ts:52`](../packages/storage/storage-domain/src/index.ts)
+来源: [`packages/storage/storage-domain/src/index.ts:52`](../packages/storage/storage-domain/src/index.ts)
## `@deepseek-ai/dsh-storage-json`
-需要:`storage`
+需要: `storage`
```ts config-catalog
/**
@@ -2789,13 +2804,13 @@ export interface Config {
}
```
-来源:[`packages/storage/storage-json/src/index.ts:28`](../packages/storage/storage-json/src/index.ts)
+来源: [`packages/storage/storage-json/src/index.ts:28`](../packages/storage/storage-json/src/index.ts)
## `@deepseek-ai/dsh-storage-sqlite`
-需要:`storage`
+需要: `storage`
```ts config-catalog
/** Plugin configuration. */
@@ -2829,7 +2844,7 @@ export interface Config {
export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
```
-来源:[`packages/storage/storage-sqlite/src/index.ts:24`](../packages/storage/storage-sqlite/src/index.ts)
+来源: [`packages/storage/storage-sqlite/src/index.ts:24`](../packages/storage/storage-sqlite/src/index.ts)
@@ -2839,19 +2854,21 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
/** Host configuration for continuable subagent capacity. */
export interface Config {
/** Maximum live children sharing uninterrupted continuable parent links; defaults to 8. */
- maxActiveSubagents?: number
+ maxActiveSubagents: Volatile
/** Default delegation depth for tools without an explicit limit; defaults to 1. */
- maxDepth?: number
+ maxDepth: Volatile
}
```
+Depends on: `Volatile` (`@deepseek-ai/cordis`)
+
来源: [`packages/subagent/subagent/src/index.ts:191`](../packages/subagent/subagent/src/index.ts)
## `@deepseek-ai/dsh-subagent-acp`
-需要:`subagents` · `subprocess`
+需要: `subagents` · `subprocess`
```ts config-catalog
/** Config: how to spawn and drive the child ACP agent process. */
@@ -2898,13 +2915,13 @@ export interface Config {
export type PermissionPolicy = 'allow' | 'reject'
```
-来源:[`packages/subagent/subagent-acp/src/index.ts:27`](../packages/subagent/subagent-acp/src/index.ts)
+来源: [`packages/subagent/subagent-acp/src/index.ts:27`](../packages/subagent/subagent-acp/src/index.ts)
## `@deepseek-ai/dsh-subagent-claude-code`
-需要:`subagents` · `subprocess`
+需要: `subagents` · `subprocess`
```ts config-catalog
/** Deployment-owned model, permission, environment, and process-release settings. */
@@ -2933,13 +2950,13 @@ export interface Config {
export type ClaudeCodePermissionMode = typeof CLAUDE_CODE_PERMISSION_MODES[number]
```
-来源:[`packages/subagent/subagent-claude-code/src/index.ts:38`](../packages/subagent/subagent-claude-code/src/index.ts)
+来源: [`packages/subagent/subagent-claude-code/src/index.ts:38`](../packages/subagent/subagent-claude-code/src/index.ts)
## `@deepseek-ai/dsh-subagent-codex`
-需要:`subagents` · `subprocess`
+需要: `subagents` · `subprocess`
```ts config-catalog
/** Deployment-owned model, permission, environment, and process-release settings. */
@@ -2966,13 +2983,13 @@ export type CodexPermissionMode =
| 'dangerously-bypass-approvals-and-sandbox'
```
-来源:[`packages/subagent/subagent-codex/src/index.ts:36`](../packages/subagent/subagent-codex/src/index.ts)
+来源: [`packages/subagent/subagent-codex/src/index.ts:36`](../packages/subagent/subagent-codex/src/index.ts)
## `@deepseek-ai/dsh-subagent-dsh-sdk`
-需要:`subagents`
+需要: `subagents`
```ts config-catalog
/** Config: how to spawn and drive the child SDK runtime process. */
@@ -3022,13 +3039,13 @@ export interface Config {
}
```
-来源:[`packages/subagent/subagent-dsh-sdk/src/index.ts:34`](../packages/subagent/subagent-dsh-sdk/src/index.ts)
+来源: [`packages/subagent/subagent-dsh-sdk/src/index.ts:34`](../packages/subagent/subagent-dsh-sdk/src/index.ts)
## `@deepseek-ai/dsh-subagent-fork-in-process`
-需要:`subagents`
+需要: `subagents`
```ts config-catalog
/** Config: the registry name to register the provider under. */
@@ -3038,13 +3055,13 @@ export interface Config {
}
```
-来源:[`packages/subagent/subagent-fork-in-process/src/index.ts:31`](../packages/subagent/subagent-fork-in-process/src/index.ts)
+来源: [`packages/subagent/subagent-fork-in-process/src/index.ts:31`](../packages/subagent/subagent-fork-in-process/src/index.ts)
## `@deepseek-ai/dsh-subagent-spawn-in-process`
-需要:`subagents`
+需要: `subagents`
```ts config-catalog
/** Config: the registry name to register the provider under. */
@@ -3054,7 +3071,7 @@ export interface Config {
}
```
-来源:[`packages/subagent/subagent-spawn-in-process/src/index.ts:25`](../packages/subagent/subagent-spawn-in-process/src/index.ts)
+来源: [`packages/subagent/subagent-spawn-in-process/src/index.ts:25`](../packages/subagent/subagent-spawn-in-process/src/index.ts)
@@ -3086,13 +3103,13 @@ export interface Config {
}
```
-来源:[`packages/core/system-prompt/src/index.ts:247`](../packages/core/system-prompt/src/index.ts)
+来源: [`packages/core/system-prompt/src/index.ts:247`](../packages/core/system-prompt/src/index.ts)
## `@deepseek-ai/dsh-terminal-bash`
-需要:`terminals` · `sandboxPolicy` · `sessionProjections` · `subprocess`
+需要: `terminals` · `sandboxPolicy` · `sessionProjections` · `subprocess`
```ts config-catalog
/** Public plugin configuration. */
@@ -3136,13 +3153,13 @@ export interface Config {
export type ShellDialect = 'bash' | 'pwsh'
```
-来源:[`packages/terminal/terminal-bash/src/config.ts:10`](../packages/terminal/terminal-bash/src/config.ts)
+来源: [`packages/terminal/terminal-bash/src/config.ts:10`](../packages/terminal/terminal-bash/src/config.ts)
## `@deepseek-ai/dsh-time-context`
-需要:`agents` · `sessionProjections`
+需要: `agents` · `sessionProjections`
```ts config-catalog
/** Request-preparation clock formatting and append scheduling. Invalid values fail plugin load. */
@@ -3154,13 +3171,13 @@ export interface Config {
}
```
-来源:[`packages/context/time-context/src/index.ts:56`](../packages/context/time-context/src/index.ts)
+来源: [`packages/context/time-context/src/index.ts:56`](../packages/context/time-context/src/index.ts)
## `@deepseek-ai/dsh-tmux-context`
-需要:`agents` · `sessionProjections`
+需要: `agents` · `sessionProjections`
```ts config-catalog
/** Per-turn tmux-location scheduling. Invalid values fail plugin load. */
@@ -3170,26 +3187,26 @@ export interface Config {
}
```
-来源:[`packages/context/tmux-context/src/index.ts:47`](../packages/context/tmux-context/src/index.ts)
+来源: [`packages/context/tmux-context/src/index.ts:47`](../packages/context/tmux-context/src/index.ts)
## `@deepseek-ai/dsh-token-meter`
-需要:`sessionProjections`
+需要: `sessionProjections`
```ts config-catalog
/** Token-meter plugin configuration; the fixed estimator has no settings. */
export type TokenMeterConfig = Record
```
-来源:[`packages/llm/token-meter/src/types.ts:13`](../packages/llm/token-meter/src/types.ts)
+来源: [`packages/llm/token-meter/src/types.ts:13`](../packages/llm/token-meter/src/types.ts)
## `@deepseek-ai/dsh-tool-bash`
-需要:`tools` · `shell` · `systemPrompt` · `shellEnv`
+需要: `tools` · `shell` · `systemPrompt` · `shellEnv`
```ts config-catalog
/** Configuration for the bash tool. */
@@ -3212,13 +3229,13 @@ export interface Config {
}
```
-来源:[`packages/shell/tool-bash/src/index.ts:37`](../packages/shell/tool-bash/src/index.ts)
+来源: [`packages/shell/tool-bash/src/index.ts:37`](../packages/shell/tool-bash/src/index.ts)
## `@deepseek-ai/dsh-tool-bash-persistent`
-需要:`tools` · `terminals`
+需要: `tools` · `terminals`
```ts config-catalog
/** Configuration for the persistent Bash tool. */
@@ -3234,13 +3251,13 @@ export interface Config {
}
```
-来源:[`packages/shell/tool-bash-persistent/src/index.ts:443`](../packages/shell/tool-bash-persistent/src/index.ts)
+来源: [`packages/shell/tool-bash-persistent/src/index.ts:443`](../packages/shell/tool-bash-persistent/src/index.ts)
## `@deepseek-ai/dsh-tool-fs`
-需要:`tools` · `fs` · `systemPrompt`
+需要: `tools` · `fs` · `systemPrompt`
```ts config-catalog
/** Plugin config (all optional — `Config` supplies the defaults). */
@@ -3256,13 +3273,13 @@ export interface Config {
}
```
-来源:[`packages/fs/tool-fs/src/index.ts:25`](../packages/fs/tool-fs/src/index.ts)
+来源: [`packages/fs/tool-fs/src/index.ts:25`](../packages/fs/tool-fs/src/index.ts)
## `@deepseek-ai/dsh-tool-fs-search`
-需要:`tools` · `systemPrompt` · `subprocess`
+需要: `tools` · `systemPrompt` · `subprocess`
```ts config-catalog
/** Plugin config; over-cap glob sampling is an explicit deployment choice and the remaining fields have defaults. */
@@ -3291,13 +3308,13 @@ export interface Config {
}
```
-来源:[`packages/fs/tool-fs-search/src/index.ts:73`](../packages/fs/tool-fs-search/src/index.ts)
+来源: [`packages/fs/tool-fs-search/src/index.ts:73`](../packages/fs/tool-fs-search/src/index.ts)
## `@deepseek-ai/dsh-tool-goal`
-需要:`agents` · `goals` · `tools` · `systemPrompt` · `sessionProjections`
+需要: `agents` · `goals` · `tools` · `systemPrompt` · `sessionProjections`
```ts config-catalog
/** Model policy and hard lower bounds for goal-state updates. */
@@ -3307,13 +3324,13 @@ export interface Config {
}
```
-来源:[`packages/goal/tool-goal/src/index.ts:32`](../packages/goal/tool-goal/src/index.ts)
+来源: [`packages/goal/tool-goal/src/index.ts:32`](../packages/goal/tool-goal/src/index.ts)
## `@deepseek-ai/dsh-tool-jobs`
-需要:`tools` · `jobs` · `systemPrompt`
+需要: `tools` · `jobs` · `systemPrompt`
```ts config-catalog
/** Configures bounded `job_output` waits and completion-notice delivery. */
@@ -3341,13 +3358,13 @@ export interface Config {
export type CompletionDelivery = 'quiet' | 'wakeup'
```
-来源:[`packages/jobs/tool-jobs/src/index.ts:41`](../packages/jobs/tool-jobs/src/index.ts)
+来源: [`packages/jobs/tool-jobs/src/index.ts:41`](../packages/jobs/tool-jobs/src/index.ts)
## `@deepseek-ai/dsh-tool-lsp`
-需要:`tools` · `lsp` · `systemPrompt`
+需要: `tools` · `lsp` · `systemPrompt`
```ts config-catalog
/** Plugin configuration: result caps and the timeout budget. */
@@ -3361,13 +3378,13 @@ export interface Config {
}
```
-来源:[`packages/lsp/tool-lsp/src/index.ts:57`](../packages/lsp/tool-lsp/src/index.ts)
+来源: [`packages/lsp/tool-lsp/src/index.ts:57`](../packages/lsp/tool-lsp/src/index.ts)
## `@deepseek-ai/dsh-tool-present`
-依赖: `tools` · `fs` · `sessionProjections`
+需要: `tools` · `fs` · `sessionProjections`
```ts config-catalog
/** Per-call delivery limit. */
@@ -3377,13 +3394,13 @@ export interface Config {
}
```
-来源:[`packages/deliverables/tool-present/src/index.ts:15`](../packages/deliverables/tool-present/src/index.ts)
+来源: [`packages/deliverables/tool-present/src/index.ts:15`](../packages/deliverables/tool-present/src/index.ts)
## `@deepseek-ai/dsh-tool-pwsh`
-需要:`tools` · `shell` · `systemPrompt` · `shellEnv`
+需要: `tools` · `shell` · `systemPrompt` · `shellEnv`
```ts config-catalog
/** Configuration for the pwsh tool. */
@@ -3406,13 +3423,13 @@ export interface Config {
}
```
-来源:[`packages/shell/tool-pwsh/src/index.ts:54`](../packages/shell/tool-pwsh/src/index.ts)
+来源: [`packages/shell/tool-pwsh/src/index.ts:54`](../packages/shell/tool-pwsh/src/index.ts)
## `@deepseek-ai/dsh-tool-pwsh-persistent`
-需要:`tools` · `terminals`
+需要: `tools` · `terminals`
```ts config-catalog
/** Configuration for the persistent pwsh tool. */
@@ -3428,13 +3445,13 @@ export interface Config {
}
```
-来源:[`packages/shell/tool-pwsh-persistent/src/index.ts:480`](../packages/shell/tool-pwsh-persistent/src/index.ts)
+来源: [`packages/shell/tool-pwsh-persistent/src/index.ts:480`](../packages/shell/tool-pwsh-persistent/src/index.ts)
## `@deepseek-ai/dsh-tool-ralph`
-需要:`tools` · `workflowEngine` · `subagents` · `systemPrompt`
+需要: `tools` · `workflowEngine` · `subagents` · `systemPrompt`
```ts config-catalog
/** Deployment policy for the fixed Ralph workflow. */
@@ -3450,13 +3467,13 @@ export interface Config {
}
```
-来源:[`packages/workflow/tool-ralph/src/index.ts:21`](../packages/workflow/tool-ralph/src/index.ts)
+来源: [`packages/workflow/tool-ralph/src/index.ts:21`](../packages/workflow/tool-ralph/src/index.ts)
## `@deepseek-ai/dsh-tool-session-query`
-需要:`tools` · `systemPrompt` · `sessionQuery` · `sessionProjections`
+需要: `tools` · `systemPrompt` · `sessionQuery` · `sessionProjections`
```ts config-catalog
/** Deployment-owned search count and timeout bounds. */
@@ -3468,13 +3485,13 @@ export interface Config {
}
```
-来源:[`packages/session-query/tool-session-query/src/index.ts:28`](../packages/session-query/tool-session-query/src/index.ts)
+来源: [`packages/session-query/tool-session-query/src/index.ts:28`](../packages/session-query/tool-session-query/src/index.ts)
## `@deepseek-ai/dsh-tool-skill`
-需要:`agents` · `tools` · `skills`
+需要: `agents` · `tools` · `skills`
```ts config-catalog
/** Model-facing skill catalog configuration. */
@@ -3484,13 +3501,13 @@ export interface Config {
}
```
-来源:[`packages/skill/tool-skill/src/index.ts:61`](../packages/skill/tool-skill/src/index.ts)
+来源: [`packages/skill/tool-skill/src/index.ts:61`](../packages/skill/tool-skill/src/index.ts)
## `@deepseek-ai/dsh-tool-str-replace-editor`
-需要:`tools` · `fs`
+需要: `tools` · `fs`
```ts config-catalog
/** Configuration for the string-replacement editor tool. */
@@ -3502,13 +3519,13 @@ export interface Config {
}
```
-来源:[`packages/fs/tool-str-replace-editor/src/index.ts:505`](../packages/fs/tool-str-replace-editor/src/index.ts)
+来源: [`packages/fs/tool-str-replace-editor/src/index.ts:505`](../packages/fs/tool-str-replace-editor/src/index.ts)
## `@deepseek-ai/dsh-tool-subagent`
-需要:`tools` · `subagents` · `systemPrompt` · `sessionProjections`
+需要: `tools` · `subagents` · `systemPrompt` · `sessionProjections`
```ts config-catalog
/** Config: which registered provider this tool delegates to, plus child defaults. */
@@ -3573,13 +3590,13 @@ export interface Config {
Depends on: [`AgentOptions`](subsystems/core.zh.md)
-来源:[`packages/subagent/tool-subagent/src/index.ts:48`](../packages/subagent/tool-subagent/src/index.ts)
+来源: [`packages/subagent/tool-subagent/src/index.ts:48`](../packages/subagent/tool-subagent/src/index.ts)
## `@deepseek-ai/dsh-tool-terminal`
-需要:`terminals` · `tools` · `systemPrompt`
+需要: `terminals` · `tools` · `systemPrompt`
```ts config-catalog
/** Model-facing terminal tool configuration. */
@@ -3591,13 +3608,13 @@ export interface Config {
}
```
-来源:[`packages/terminal/tool-terminal/src/index.ts:36`](../packages/terminal/tool-terminal/src/index.ts)
+来源: [`packages/terminal/tool-terminal/src/index.ts:36`](../packages/terminal/tool-terminal/src/index.ts)
## `@deepseek-ai/dsh-tool-todo`
-需要:`tools`
+需要: `tools` · `sessionProjections`
```ts config-catalog
/** Model-facing todo tool configuration. */
@@ -3613,13 +3630,13 @@ export interface Config {
}
```
-来源:[`packages/todo/tool-todo/src/index.ts:29`](../packages/todo/tool-todo/src/index.ts)
+来源: [`packages/todo/tool-todo/src/index.ts:29`](../packages/todo/tool-todo/src/index.ts)
## `@deepseek-ai/dsh-tool-web`
-需要:`tools` · `web` · `systemPrompt`
+需要: `tools` · `web` · `systemPrompt`
```ts config-catalog
/** Plugin config: which web tools to register, search bounds, per-tool budgets, and the fetch output cap. */
@@ -3641,13 +3658,13 @@ export interface Config {
}
```
-来源:[`packages/web/tool-web/src/index.ts:37`](../packages/web/tool-web/src/index.ts)
+来源: [`packages/web/tool-web/src/index.ts:37`](../packages/web/tool-web/src/index.ts)
## `@deepseek-ai/dsh-tool-workflow`
-需要:`tools` · `workflowEngine` · `systemPrompt`
+需要: `tools` · `workflowEngine` · `systemPrompt`
```ts config-catalog
/** Config: the model-facing tool name plus result rendering caps. */
@@ -3667,13 +3684,13 @@ export interface Config {
}
```
-来源:[`packages/workflow/tool-workflow/src/index.ts:32`](../packages/workflow/tool-workflow/src/index.ts)
+来源: [`packages/workflow/tool-workflow/src/index.ts:44`](../packages/workflow/tool-workflow/src/index.ts)
## `@deepseek-ai/dsh-tool-workspace-dependencies`
-依赖:`tools`
+需要:`tools`
```ts config-catalog
/** Payload location and optional installation directory. */
@@ -3695,7 +3712,7 @@ export interface Config {
## `@deepseek-ai/dsh-tools`
-需要:`systemPrompt`
+需要: `systemPrompt`
```ts config-catalog
/** Plugin config: how the registered tools are presented to the model. */
@@ -3725,13 +3742,13 @@ export interface Config {
export type ToolPresentationMode = 'native' | 'ptc' | 'both'
```
-来源:[`packages/core/tools/src/index.ts:663`](../packages/core/tools/src/index.ts)
+来源: [`packages/core/tools/src/index.ts:663`](../packages/core/tools/src/index.ts)
## `@deepseek-ai/dsh-typert-loader`
-需要:`typert` · `loader`
+需要: `typert` · `loader`
```ts config-catalog
/** Additional package artifacts whose owning plugins are nested behind another Loader entry. */
@@ -3741,7 +3758,7 @@ export interface Config {
}
```
-来源:[`packages/typert/loader/src/index.ts:48`](../packages/typert/loader/src/index.ts)
+来源: [`packages/typert/loader/src/index.ts:48`](../packages/typert/loader/src/index.ts)
@@ -3772,7 +3789,7 @@ export interface Config {
export type ApprovalPolicy = 'ask' | 'never'
```
-来源:[`packages/interaction/user-approval/src/index.ts:135`](../packages/interaction/user-approval/src/index.ts)
+来源: [`packages/interaction/user-approval/src/index.ts:135`](../packages/interaction/user-approval/src/index.ts)
@@ -3793,13 +3810,13 @@ export interface WebRuntimeConfig {
}
```
-来源:[`packages/web/web/src/index.ts:55`](../packages/web/web/src/index.ts)
+来源: [`packages/web/web/src/index.ts:55`](../packages/web/web/src/index.ts)
## `@deepseek-ai/dsh-web-app`
-需要:`webServer`
+需要: `webServer`
```ts config-catalog
/** Plugin config: composed deployment settings plus per-invocation command-line values. */
@@ -3820,13 +3837,13 @@ export interface Config {
}
```
-来源:[`packages/bundle/web-app/src/index.ts:44`](../packages/bundle/web-app/src/index.ts)
+来源: [`packages/bundle/web-app/src/index.ts:44`](../packages/bundle/web-app/src/index.ts)
## `@deepseek-ai/dsh-web-fetch-http`
-需要:`web`
+需要: `web`
```ts config-catalog
/** Plugin config: the provider's transport and size limits plus its `User-Agent` (all defaulted). */
@@ -3844,41 +3861,43 @@ export interface Config {
}
```
-来源:[`packages/web/web-fetch-http/src/index.ts:32`](../packages/web/web-fetch-http/src/index.ts)
+来源: [`packages/web/web-fetch-http/src/index.ts:32`](../packages/web/web-fetch-http/src/index.ts)
## `@deepseek-ai/dsh-web-search-deepseek`
-需要:`web`
+需要: `web`
```ts config-catalog
/** Plugin config (all optional — `apply` fills env-var and constant defaults). */
export interface Config {
/** Literal DeepSeek API key; prefer {@link apiKeyEnv} so no secret enters configuration files. */
- apiKey?: string
+ apiKey: Volatile
/** Credential reference resolved for each search; defaults to `DEEPSEEK_API_KEY`. */
- apiKeyEnv?: string
+ apiKeyEnv: Volatile
/** Anthropic-compatible endpoint base; `/messages` is appended. */
- baseURL?: string
+ baseURL: Volatile
/** Anthropic-format model name. Defaults to `deepseek-v4-flash`. */
- model?: string
+ model: Volatile
/** `anthropic-version` header value. Defaults to `2023-06-01`. */
- apiVersion?: string
+ apiVersion: Volatile
/** Upper bound on generated tokens for the Messages request. Defaults to 4096. */
- maxTokens?: number
+ maxTokens: Volatile
/** Maximum `web_search` server-tool uses per request. Defaults to 5. */
- maxUses?: number
+ maxUses: Volatile
}
```
-来源:[`packages/web/web-search-deepseek/src/index.ts:46`](../packages/web/web-search-deepseek/src/index.ts)
+Depends on: `Volatile` (`@deepseek-ai/cordis`)
+
+来源: [`packages/web/web-search-deepseek/src/index.ts:46`](../packages/web/web-search-deepseek/src/index.ts)
## `@deepseek-ai/dsh-web-search-exa`
-需要:`web`
+需要: `web`
```ts config-catalog
/** Plugin config (all optional — `apply` fills env-var and constant defaults). */
@@ -3896,13 +3915,13 @@ export interface Config {
}
```
-来源:[`packages/web/web-search-exa/src/index.ts:35`](../packages/web/web-search-exa/src/index.ts)
+来源: [`packages/web/web-search-exa/src/index.ts:35`](../packages/web/web-search-exa/src/index.ts)
## `@deepseek-ai/dsh-web-search-perplexity`
-需要:`web`
+需要: `web`
```ts config-catalog
/** Plugin config (all optional — `apply` fills env-var and constant defaults). */
@@ -3920,13 +3939,13 @@ export interface Config {
}
```
-来源:[`packages/web/web-search-perplexity/src/index.ts:30`](../packages/web/web-search-perplexity/src/index.ts)
+来源: [`packages/web/web-search-perplexity/src/index.ts:30`](../packages/web/web-search-perplexity/src/index.ts)
## `@deepseek-ai/dsh-webhook-github`
-需要:`webServer` · `webhookRuntime` · `credentials`
+需要: `webServer` · `webhookRuntime` · `credentials`
```ts config-catalog
/** Required GitHub ingress configuration. */
@@ -3942,7 +3961,7 @@ export interface Config {
}
```
-来源:[`packages/webhook/webhook-github/src/index.ts:17`](../packages/webhook/webhook-github/src/index.ts)
+来源: [`packages/webhook/webhook-github/src/index.ts:17`](../packages/webhook/webhook-github/src/index.ts)
@@ -3972,7 +3991,7 @@ export interface Config {
## `@deepseek-ai/dsh-workspace-changes`
-依赖: `subprocess`
+需要: `subprocess`
```ts config-catalog
/** Snapshot, capture, and comparison bounds. Invalid values fail plugin load. */
@@ -4003,7 +4022,7 @@ export interface Config {
- `@deepseek-ai/dsh-agent`([`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts))
- `@deepseek-ai/dsh-api-remotes` — 需要 `typertGateway`([`packages/api/remotes/src/index.ts`](../packages/api/remotes/src/index.ts))
- `@deepseek-ai/dsh-authorization` — 需要 `credentials`([`packages/credentials/authorization/src/index.ts`](../packages/credentials/authorization/src/index.ts))
-- `@deepseek-ai/dsh-browser-use` ([`packages/browser-use/browser-use/src/index.ts`](../packages/browser-use/browser-use/src/index.ts))
+- `@deepseek-ai/dsh-browser-use`([`packages/browser-use/browser-use/src/index.ts`](../packages/browser-use/browser-use/src/index.ts))
- `@deepseek-ai/dsh-client-file-upload` — 需要 `agents` · `attachments` · `commands` · `connection`([`packages/client/file-upload/src/index.ts`](../packages/client/file-upload/src/index.ts))
- `@deepseek-ai/dsh-client-locale`([`packages/client/locale/src/index.ts`](../packages/client/locale/src/index.ts))
- `@deepseek-ai/dsh-client-modules` — 需要 `loader`([`packages/client/modules/src/index.ts`](../packages/client/modules/src/index.ts))
@@ -4049,7 +4068,6 @@ export interface Config {
- `@deepseek-ai/dsh-client-ui-sidebar-terminal`([`packages/client/ui-sidebar-terminal/src/index.ts`](../packages/client/ui-sidebar-terminal/src/index.ts))
- `@deepseek-ai/dsh-client-ui-skill`([`packages/client/ui-skill/src/index.ts`](../packages/client/ui-skill/src/index.ts))
- `@deepseek-ai/dsh-client-ui-subagent`([`packages/client/ui-subagent/src/index.ts`](../packages/client/ui-subagent/src/index.ts))
-- `@deepseek-ai/dsh-client-ui-theme`([`packages/client/ui-theme/src/index.ts`](../packages/client/ui-theme/src/index.ts))
- `@deepseek-ai/dsh-client-ui-tool`([`packages/client/ui-tool/src/index.ts`](../packages/client/ui-tool/src/index.ts))
- `@deepseek-ai/dsh-client-ui-trajectory`([`packages/client/ui-trajectory/src/index.ts`](../packages/client/ui-trajectory/src/index.ts))
- `@deepseek-ai/dsh-client-ui-user-questions`([`packages/client/ui-user-questions/src/index.ts`](../packages/client/ui-user-questions/src/index.ts))
@@ -4060,13 +4078,14 @@ export interface Config {
- `@deepseek-ai/dsh-command-goal` — 需要 `commands` · `goals`([`packages/goal/command-goal/src/index.ts`](../packages/goal/command-goal/src/index.ts))
- `@deepseek-ai/dsh-commands`([`packages/interaction/commands/src/index.ts`](../packages/interaction/commands/src/index.ts))
- `@deepseek-ai/dsh-compaction-image-offload`,需要 `agents` 和 `sessions`([`packages/compaction/compaction-image-offload/src/index.ts`](../packages/compaction/compaction-image-offload/src/index.ts))
-- `@deepseek-ai/dsh-computer-use` ([`packages/computer-use/computer-use/src/index.ts`](../packages/computer-use/computer-use/src/index.ts))
+- `@deepseek-ai/dsh-computer-use`([`packages/computer-use/computer-use/src/index.ts`](../packages/computer-use/computer-use/src/index.ts))
+- `@deepseek-ai/dsh-config-editor` — requires `loader` · `profileContext`([`packages/boot/config-editor/src/index.ts`](../packages/boot/config-editor/src/index.ts))
- `@deepseek-ai/dsh-cordis-client-runner`([`packages/extensions/cordis-client-runner/src/index.ts`](../packages/extensions/cordis-client-runner/src/index.ts))
- `@deepseek-ai/dsh-deepseek-llm-api-extensions`([`packages/llm/deepseek-llm-api-extensions/src/index.ts`](../packages/llm/deepseek-llm-api-extensions/src/index.ts))
- `@deepseek-ai/dsh-experimental-auto-review` — 需要 `llm` · `permissionPresets` · `sessions` · `tools`([`packages/experimental/auto-review/src/index.ts`](../packages/experimental/auto-review/src/index.ts))
- `@deepseek-ai/dsh-experimental-client-ui-agent-team`([`packages/experimental/client-ui-agent-team/src/index.ts`](../packages/experimental/client-ui-agent-team/src/index.ts))
-- `@deepseek-ai/dsh-experimental-client-ui-voice-input` ([`packages/experimental/client-ui-voice-input/src/index.ts`](../packages/experimental/client-ui-voice-input/src/index.ts))
-- `@deepseek-ai/dsh-experimental-computer-use-cua-driver-native` — requires `computerUse` · `tools` · `systemPrompt` ([`packages/experimental/computer-use-cua-driver-native/src/index.ts`](../packages/experimental/computer-use-cua-driver-native/src/index.ts))
+- `@deepseek-ai/dsh-experimental-client-ui-voice-input`([`packages/experimental/client-ui-voice-input/src/index.ts`](../packages/experimental/client-ui-voice-input/src/index.ts))
+- `@deepseek-ai/dsh-experimental-computer-use-cua-driver-native` — requires `computerUse` · `tools` · `systemPrompt`([`packages/experimental/computer-use-cua-driver-native/src/index.ts`](../packages/experimental/computer-use-cua-driver-native/src/index.ts))
- `@deepseek-ai/dsh-fs-observation-policy`([`packages/fs/fs-observation-policy/src/index.ts`](../packages/fs/fs-observation-policy/src/index.ts))
- `@deepseek-ai/dsh-fs-ssh` — 需要 `ssh` · `sandboxPolicy`([`packages/ssh/fs-ssh/src/index.ts`](../packages/ssh/fs-ssh/src/index.ts))
- `@deepseek-ai/dsh-goal-round-driver` — 需要 `agents` · `goals` · `sessions`([`packages/goal/goal-round-driver/src/index.ts`](../packages/goal/goal-round-driver/src/index.ts))
@@ -4083,6 +4102,7 @@ export interface Config {
- `@deepseek-ai/dsh-session-projection`([`packages/session/session-projection/src/index.ts`](../packages/session/session-projection/src/index.ts))
- `@deepseek-ai/dsh-session-stats` — 需要 `sessionProjections`([`packages/session/session-stats/src/index.ts`](../packages/session/session-stats/src/index.ts))
- `@deepseek-ai/dsh-session-turn-outline` — 需要 `sessionProjections`([`packages/session/session-turn-outline/src/index.ts`](../packages/session/session-turn-outline/src/index.ts))
+- `@deepseek-ai/dsh-settings` — requires `configEditor` · `profileContext`([`packages/settings/settings/src/index.ts`](../packages/settings/settings/src/index.ts))
- `@deepseek-ai/dsh-skill-badge` — 需要 `skills`([`packages/skill/skill-badge/src/index.ts`](../packages/skill/skill-badge/src/index.ts))
- `@deepseek-ai/dsh-storage`([`packages/storage/storage/src/index.ts`](../packages/storage/storage/src/index.ts))
- `@deepseek-ai/dsh-subprocess-local`([`packages/subprocess/subprocess-local/src/index.ts`](../packages/subprocess/subprocess-local/src/index.ts))
@@ -4102,7 +4122,7 @@ export interface Config {
- `@deepseek-ai/dsh-attachment` — 抽象 `AttachmentStore`([`packages/attachment/attachment/src/index.ts`](../packages/attachment/attachment/src/index.ts))
- `@deepseek-ai/dsh-compaction` — 抽象 `CompactionEngine`([`packages/compaction/compaction/src/index.ts`](../packages/compaction/compaction/src/index.ts))
-- `@deepseek-ai/dsh-credentials` — 抽象 `Credentials`([`packages/credentials/credentials/src/index.ts`](../packages/credentials/credentials/src/index.ts))
+- `@deepseek-ai/dsh-credentials` — 抽象 `CredentialProvider`([`packages/credentials/credentials/src/index.ts`](../packages/credentials/credentials/src/index.ts))
- `@deepseek-ai/dsh-file-reference` — 抽象 `FileReferenceService`([`packages/context/file-reference/src/index.ts`](../packages/context/file-reference/src/index.ts))
- `@deepseek-ai/dsh-fs` — 抽象 `FileSystem`([`packages/fs/fs/src/index.ts`](../packages/fs/fs/src/index.ts))
- `@deepseek-ai/dsh-host-directory-picker` — 抽象 `DirectoryPicker`([`packages/host/directory-picker/src/index.ts`](../packages/host/directory-picker/src/index.ts))
@@ -4111,11 +4131,11 @@ export interface Config {
- `@deepseek-ai/dsh-sandbox` — 抽象 `SandboxProvider`([`packages/sandbox/sandbox/src/index.ts`](../packages/sandbox/sandbox/src/index.ts))
- `@deepseek-ai/dsh-session-persistence` — 抽象 `SessionPersistence`([`packages/session/session-persistence/src/index.ts`](../packages/session/session-persistence/src/index.ts))
- `@deepseek-ai/dsh-session-query` — 抽象 `SessionQueryEngine`([`packages/session-query/session-query/src/index.ts`](../packages/session-query/session-query/src/index.ts))
-- `@deepseek-ai/dsh-settings` — 抽象 `Settings`([`packages/settings/settings/src/index.ts`](../packages/settings/settings/src/index.ts))
- `@deepseek-ai/dsh-shell` — 抽象 `ShellExecutor`([`packages/shell/shell/src/index.ts`](../packages/shell/shell/src/index.ts))
- `@deepseek-ai/dsh-spill` — 抽象 `SpillStore`([`packages/spill/spill/src/index.ts`](../packages/spill/spill/src/index.ts))
- `@deepseek-ai/dsh-subprocess` — 抽象 `SubprocessRuntime`([`packages/subprocess/subprocess/src/index.ts`](../packages/subprocess/subprocess/src/index.ts))
- `@deepseek-ai/dsh-workflow` — 抽象 `WorkflowEngine`([`packages/workflow/workflow/src/index.ts`](../packages/workflow/workflow/src/index.ts))
+
## 库包(无插件入口)
由其他包作为库导入;`cordis.yml` 无法加载它们。
@@ -4136,7 +4156,7 @@ export interface Config {
- `@deepseek-ai/dsh-cmdline`([`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts))
- `@deepseek-ai/dsh-deque`([`packages/util/deque/src/index.ts`](../packages/util/deque/src/index.ts))
- `@deepseek-ai/dsh-experimental-agent-team-profile`([`packages/experimental/agent-team-profile/src/index.ts`](../packages/experimental/agent-team-profile/src/index.ts))
-- `@deepseek-ai/dsh-experimental-browser-use-runtime` ([`packages/experimental/browser-use-runtime/src/index.ts`](../packages/experimental/browser-use-runtime/src/index.ts))
+- `@deepseek-ai/dsh-experimental-browser-use-runtime`([`packages/experimental/browser-use-runtime/src/index.ts`](../packages/experimental/browser-use-runtime/src/index.ts))
- `@deepseek-ai/dsh-experimental-voice-input-bundle`([`packages/experimental/voice-input-bundle/src/index.ts`](../packages/experimental/voice-input-bundle/src/index.ts))
- `@deepseek-ai/dsh-experimental-webworker-packer`([`packages/experimental/webworker-packer/src/index.ts`](../packages/experimental/webworker-packer/src/index.ts))
- `@deepseek-ai/dsh-experimental-webworker-runtime`([`packages/experimental/webworker-runtime/src/index.ts`](../packages/experimental/webworker-runtime/src/index.ts))
@@ -4149,7 +4169,7 @@ export interface Config {
- `@deepseek-ai/dsh-loader-smoke`([`packages/test-support/loader-smoke/src/index.ts`](../packages/test-support/loader-smoke/src/index.ts))
- `@deepseek-ai/dsh-native-command`([`packages/util/native-command/src/index.ts`](../packages/util/native-command/src/index.ts))
- `@deepseek-ai/dsh-output-retention`([`packages/util/output-retention/src/index.ts`](../packages/util/output-retention/src/index.ts))
-- `@deepseek-ai/dsh-package-manifest` ([`packages/util/package-manifest/src/index.ts`](../packages/util/package-manifest/src/index.ts))
+- `@deepseek-ai/dsh-package-manifest`([`packages/util/package-manifest/src/index.ts`](../packages/util/package-manifest/src/index.ts))
- `@deepseek-ai/dsh-remote-mock`([`packages/test-support/remote-mock/src/index.ts`](../packages/test-support/remote-mock/src/index.ts))
- `@deepseek-ai/dsh-sandbox-windows-acl`([`packages/sandbox/sandbox-windows-acl/src/index.ts`](../packages/sandbox/sandbox-windows-acl/src/index.ts))
- `@deepseek-ai/dsh-scope`([`packages/core/scope/src/index.ts`](../packages/core/scope/src/index.ts))
diff --git a/docs/cookbook/adding-a-settings-card.i18n.yaml b/docs/cookbook/adding-a-settings-card.i18n.yaml
index 1c493f63fe..0f8a5b63ed 100644
--- a/docs/cookbook/adding-a-settings-card.i18n.yaml
+++ b/docs/cookbook/adding-a-settings-card.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/cookbook/adding-a-settings-card.md
-adding-a-settings-card.md: 72d7eaeb85d0d4f6981e814a8c335428c561b6bd
-adding-a-settings-card.zh.md: d1254078085ba1de541ea70db566bcfb1bd50d1d
+adding-a-settings-card.md: 3e473d8bbc52d0bbfddcabc2512dbef1b23a8fb4
+adding-a-settings-card.zh.md: fd2c71366112b7f89c1a7f3de000a28778c44ee7
diff --git a/docs/cookbook/adding-a-settings-card.md b/docs/cookbook/adding-a-settings-card.md
index 72d7eaeb85..3e473d8bbc 100644
--- a/docs/cookbook/adding-a-settings-card.md
+++ b/docs/cookbook/adding-a-settings-card.md
@@ -1,86 +1,47 @@
-# Cookbook: adding a settings page
+# Cookbook: live configuration forms
English | [中文](adding-a-settings-card.zh.md)
-How a plugin puts its own configuration on the web client's Plugins page, and how it contributes to another plugin's page there. Nothing in this path needs a change inside this repository: the Host serves every registered settings namespace, and the Plugins page declares the slots a page or a contribution registers into.
+Declare live fields in the plugin Config schema and expose them through a product-owned settings card. The exported `Config` interface describes the values received by the plugin, including each `Volatile` reference.
-The two halves of a community bundle live in one package — the Host half under `src/`, the browser half under `src/client/`, exported as `./client` and declared with `dsh.client`. A built-in plugin whose package cannot carry a browser half ships its page as a companion client package instead; [`packages/client/ui-settings-shell`](../../packages/client/ui-settings-shell) is the template.
-
-## 1. Register the namespace (Host half)
-
-The namespace is the join key, so pick it once and spell it in both halves. A consumer that already has a `cordis.yml` entry should register through `ctx.settings.installSection()`, which layers the entry under the user document and keeps working when no settings provider is mounted:
+## 1. Declare live fields
```ts
-import type { Context } from '@deepseek-ai/cordis'
-import type {} from '@deepseek-ai/dsh-settings'
+import type { Context, Volatile } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
-declare function assertReachable(endpoint: string | undefined): void
-declare function rebuildFromSettings(config: Config): void
-
-export const MY_PLUGIN_NS = 'my-plugin'
-
export interface Config {
- endpoint?: string
- retries?: number
+ endpoint: Volatile
+ retries: Volatile
}
-export const Config: z = z.object({
- endpoint: z.string(),
- retries: z.number().step(1).min(0).default(3),
+export const Config = z.object({
+ endpoint: z.string().volatile(),
+ retries: z.number().step(1).min(0).default(3).volatile(),
})
-export function apply(ctx: Context, config: Config) {
- let source = () => config
- ctx.inject(['settings'], (settingsCtx) => {
- settingsCtx.settings.installSection(ctx, MY_PLUGIN_NS, Config, config, {
- // Constraints the schema cannot express refuse the write, not the next use.
- validate: value => void assertReachable(value.endpoint),
- setSource: (current) => { source = current },
- onChange: () => { rebuildFromSettings(source()) },
- })
+export function apply(ctx: Context, config: Config): void {
+ ctx.on('loader/volatile-update', () => {
+ ctx.logger.info('Retry limit: %d', config.retries.get())
})
}
```
-`role('secret')` on a field keeps its value off every response; the page writes such a field through the `credentials` domain instead. `applies: 'restart'` tells a configuration surface the owner acts on a change only at the next start.
+Read `.get()` when starting an operation. A request that needs a consistent snapshot captures its values once. Use `.check()` for cross-field Config validation; these checks run on the Host before persistence and are omitted from serialized form schemas.
-## 2. Register the page (browser half)
+## 2. Compose the plugin
-The page registers into one of the Plugins page's configuration slots and owns everything inside it — controls, copy, and styles. A bundle's own configuration goes into `plugins.bundle.config`, keyed by the package name; the configuration of one row the bundle declares goes into `plugins.row.config`, keyed by `#`, which gives that row a configure control opening the row's page. The page asks every entry for two views: `summary` for the one-liner under the title, `page` for the form.
+Give each instance a unique profile entry id. The base bundle mounts settings and config-editor. For custom profiles, see their package READMEs before mounting the same services.
-The form reads and writes through `ctx.settingsScope`, which fences each write with the revision it read. The `ui-primitives` kit carries the staged-edit model and the controls, so a page declares its fields and renders them:
+`role('secret')` keeps a value out of form responses. Use credential references for values managed by the credentials domain. Ordinary fields stay out of the settings schema.
-```ts ignore-check
-import type { Context as ClientContext } from '@deepseek-ai/cordis'
-// Type-only: the Plugins page's slot declarations and the settings scope.
-// Cross-plugin collaboration goes through cordis services; a value import
-// fails the client bundle-purity gate.
-import type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/client'
-import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
-import { SettingsFormModel, settingsNumberField, settingsTextField } from '@deepseek-ai/dsh-client-ui-primitives'
+## 3. Verify editing
-export const inject = ['slots', 'locale', 'settingsScope']
+Change a field in Plugins and save. Verify the profile patch, the next consumer operation, unchanged plugin instance identity, and restoration after restart. Reject an invalid value and verify that neither the file nor the live value changed.
-export function apply(ctx: ClientContext): void {
- ctx.effect(() => ctx.locale.register('myPlugin', { zh, en }))
- const form = new SettingsFormModel(
- ctx.settingsScope.bind({ namespace: 'my-plugin' }),
- [settingsTextField('endpoint'), settingsNumberField('retries')],
- )
- const store = form.bind(() => ({ ...form.shell(), endpoint: form.field('endpoint'), retries: form.field('retries') }))
- ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
- name: 'plugins.bundle.config',
- key: '@acme/dsh-my-plugin',
- locale: 'myPlugin',
- inject: () => ({ hooks: { myPluginForm: store }, ...form.actions() }),
- }, MyPluginPage))
-}
-```
+Custom plugin pages receive `form.state` and `form.mutate(operations, expectedRevision)` from the Plugins page owner. The Host validates full Config and applies edits through ConfigEditor and volatile HMR. The [plugin settings package](../../packages/client/ui-settings-plugins/README.md) owns the existing card examples.
-`MyPluginPage` renders `t('summary')` for `view === 'summary'` and, for `page`, a `SettingsForm` around `SettingsValueField` controls, passing the frame its copy as `labels`. A field's presence in the raw user layer — not its value — is what marks it overridden; a reset clears that field so it re-inherits the composition layer. Only a save writes, and leaving the page drops every draft.
-
-## 3. Contribute to another plugin's page
+## 4. Contribute to another plugin's page
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:
@@ -92,7 +53,7 @@ ctx.slots.inject('plugins.detail.badge', () => ctx.slots.register({
}, ({ t, subject }) => subject.kind === 'bundle' && hasUpdate(subject.pkg) ? {t('update')} : null))
```
-## 4. Where the browser half rides
+## 5. Where the browser half rides
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.
@@ -108,4 +69,4 @@ The built `./client` file must be in the client module system's lazy-CJS factory
}
```
-A companion package for a built-in namespace is the same browser half in a client-only package with an empty Host `apply`, listed in the web composition's plugin roster ([`packages/bundle/web-app/cordis.patch.yml`](../../packages/bundle/web-app/cordis.patch.yml)) and registering into `plugins.item` through `ctx.settingsScope.whileServed`, so the page exists exactly while the Host serves the namespace.
+A companion package for a built-in namespace is the same browser half in a client-only package with an empty Host `apply`, listed in the web composition's plugin roster ([`packages/bundle/web-app/cordis.patch.yml`](../../packages/bundle/web-app/cordis.patch.yml)) and registering into `plugins.item` through `ctx.configForms.whileServed`, so the page exists exactly while the Host serves the namespace.
diff --git a/docs/cookbook/adding-a-settings-card.zh.md b/docs/cookbook/adding-a-settings-card.zh.md
index d125407808..fd2c713661 100644
--- a/docs/cookbook/adding-a-settings-card.zh.md
+++ b/docs/cookbook/adding-a-settings-card.zh.md
@@ -1,86 +1,47 @@
-# 手册:添加设置页
+# 实践指南:即时配置表单
[English](adding-a-settings-card.md) | 中文
-插件如何把自己的配置放到 Web 客户端的插件页上,以及如何向那里的其他插件页面贡献内容。这条路径不需要改动本仓库:Host 服务每一个已注册的设置命名空间,插件页声明了页面或贡献要注册进去的 slot。
+在插件 Config schema 中声明即时字段,并通过产品所属的设置卡片提供编辑入口。导出的 `Config` 接口描述插件收到的值,包括每个 `Volatile` 引用。
-社区组合包的两个半侧放在一个包里——宿主半侧在 `src/`,浏览器半侧在 `src/client/`,导出为 `./client` 并用 `dsh.client` 声明。包本身带不了浏览器半侧的内置插件,则把页面做成一个伴生客户端包;[`packages/client/ui-settings-shell`](../../packages/client/ui-settings-shell) 是模板。
-
-## 1. 注册命名空间(宿主半侧)
-
-命名空间是连接键,选定一次,两个半侧拼写一致。已有 `cordis.yml` 条目的消费者应通过 `ctx.settings.installSection()` 注册,它把条目铺在用户文档之下,并在没有挂载设置提供方时照常工作:
+## 1. 声明即时字段
```ts
-import type { Context } from '@deepseek-ai/cordis'
-import type {} from '@deepseek-ai/dsh-settings'
+import type { Context, Volatile } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
-declare function assertReachable(endpoint: string | undefined): void
-declare function rebuildFromSettings(config: Config): void
-
-export const MY_PLUGIN_NS = 'my-plugin'
-
export interface Config {
- endpoint?: string
- retries?: number
+ endpoint: Volatile
+ retries: Volatile
}
-export const Config: z = z.object({
- endpoint: z.string(),
- retries: z.number().step(1).min(0).default(3),
+export const Config = z.object({
+ endpoint: z.string().volatile(),
+ retries: z.number().step(1).min(0).default(3).volatile(),
})
-export function apply(ctx: Context, config: Config) {
- let source = () => config
- ctx.inject(['settings'], (settingsCtx) => {
- settingsCtx.settings.installSection(ctx, MY_PLUGIN_NS, Config, config, {
- // Constraints the schema cannot express refuse the write, not the next use.
- validate: value => void assertReachable(value.endpoint),
- setSource: (current) => { source = current },
- onChange: () => { rebuildFromSettings(source()) },
- })
+export function apply(ctx: Context, config: Config): void {
+ ctx.on('loader/volatile-update', () => {
+ ctx.logger.info('Retry limit: %d', config.retries.get())
})
}
```
-字段上的 `role('secret')` 让它的值不出现在任何响应里;页面改经 `credentials` 域写入这类字段。`applies: 'restart'` 告诉配置界面:所有者要到下次启动才响应更改。
+在操作开始时读取 `.get()`。需要一致快照的请求一次性捕获所需值。使用 `.check()` 进行跨字段 Config 验证;这些检查在持久化前由 Host 执行,不进入序列化的表单 schema。
-## 2. 注册页面(浏览器半侧)
+## 2. 组合插件
-页面注册进插件页的某个配置 slot,并拥有其中的一切——控件、文案和样式。组合包自己的配置进 `plugins.bundle.config`,以包名为键;组合包声明的某一行的配置进 `plugins.row.config`,以 `<包名>#<行 id>` 为键,这会给该行一个打开行页的配置控件。页面向每个条目索取两种视图:`summary` 是标题下的一句话,`page` 是表单。
+为每个实例分配唯一的 profile 条目 id。基础组合包挂载 settings 和 config-editor。自定义 profile 挂载这些服务前,请阅读相应包的 README。
-表单通过 `ctx.settingsScope` 读写,每次写入都以读取时的修订号作栅栏。`ui-primitives` 套件提供暂存编辑模型和控件,页面只需声明字段并渲染:
+`role('secret')` 阻止值进入表单响应。对于凭据域管理的值,使用凭据引用。普通字段不进入设置 schema。
-```ts ignore-check
-import type { Context as ClientContext } from '@deepseek-ai/cordis'
-// Type-only: the Plugins page's slot declarations and the settings scope.
-// Cross-plugin collaboration goes through cordis services; a value import
-// fails the client bundle-purity gate.
-import type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/client'
-import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
-import { SettingsFormModel, settingsNumberField, settingsTextField } from '@deepseek-ai/dsh-client-ui-primitives'
+## 3. 验证编辑
-export const inject = ['slots', 'locale', 'settingsScope']
+在插件页面修改字段并保存。验证 profile patch、消费者下一次操作、插件实例标识不变,以及重启后的恢复。提交无效值,并确认文件和即时值均未改变。
-export function apply(ctx: ClientContext): void {
- ctx.effect(() => ctx.locale.register('myPlugin', { zh, en }))
- const form = new SettingsFormModel(
- ctx.settingsScope.bind({ namespace: 'my-plugin' }),
- [settingsTextField('endpoint'), settingsNumberField('retries')],
- )
- const store = form.bind(() => ({ ...form.shell(), endpoint: form.field('endpoint'), retries: form.field('retries') }))
- ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
- name: 'plugins.bundle.config',
- key: '@acme/dsh-my-plugin',
- locale: 'myPlugin',
- inject: () => ({ hooks: { myPluginForm: store }, ...form.actions() }),
- }, MyPluginPage))
-}
-```
+自定义插件页面从 Plugins 页面所有者接收 `form.state` 和 `form.mutate(operations, expectedRevision)`。Host 校验完整 Config,并通过 ConfigEditor 和 volatile HMR 应用修改。[插件设置包](../../packages/client/ui-settings-plugins/README.zh.md) 提供现有卡片示例。
-`MyPluginPage` 在 `view === 'summary'` 时渲染 `t('summary')`,在 `page` 时渲染一个包着 `SettingsValueField` 控件的 `SettingsForm`,并以 `labels` 把文案交给框架。字段是否被覆盖看它在原始用户层里是否存在,而不看它的值;重置会清除该字段,让它重新继承组合层。只有保存才写入,离开页面即丢弃所有草稿。
-
-## 3. 向其他插件的页面贡献内容
+## 4. 向其他插件的页面贡献内容
对某个不属于自己的组合包、行或官方插件有话要说的插件,注册进 `plugins.detail.actions`(页头的控件)、`plugins.detail.badge`(标题旁的标签)或 `plugins.detail.section`(页面自身内容之下的区块)。每个条目都以页面的 `subject` 渲染——`{ kind: 'bundle', pkg }`、`{ kind: 'row', pkg, row }` 或 `{ kind: 'item', id }`——对无话可说的 subject 返回 null:
@@ -92,7 +53,7 @@ ctx.slots.inject('plugins.detail.badge', () => ctx.slots.register({
}, ({ t, subject }) => subject.kind === 'bundle' && hasUpdate(subject.pkg) ? {t('update')} : null))
```
-## 4. 浏览器半侧挂在哪里
+## 5. 浏览器半侧挂在哪里
浏览器半侧由[客户端模块系统](../../packages/client/modules)送到页面:它扫描已启用的 Loader 条目,找出声明了 `dsh.client` 的包,送出每个包构建好的 `./client` 导出——但它只把一个包的半侧挂在说明符恰为裸包名的那一行上。从子路径导出挂载的行永远不带半侧,因此把一个包拆成多行的组合包,其半侧留在根行上,它注册的每个页面都随根行关闭而消失。需要在其他行关闭时仍保留页面的子插件,应作为独立的包发布。
@@ -108,4 +69,4 @@ ctx.slots.inject('plugins.detail.badge', () => ctx.slots.register({
}
```
-内置命名空间的伴生包就是同样的浏览器半侧放在一个纯客户端包里,宿主 `apply` 为空,列入 Web 组合的插件花名册([`packages/bundle/web-app/cordis.patch.yml`](../../packages/bundle/web-app/cordis.patch.yml)),并通过 `ctx.settingsScope.whileServed` 注册进 `plugins.item`,页面因此恰好在 Host 服务该命名空间期间存在。
+内置命名空间的伴生包就是同样的浏览器半侧放在一个纯客户端包里,宿主 `apply` 为空,列入 Web 组合的插件花名册([`packages/bundle/web-app/cordis.patch.yml`](../../packages/bundle/web-app/cordis.patch.yml)),并通过 `ctx.configForms.whileServed` 注册进 `plugins.item`,页面因此恰好在 Host 服务该命名空间期间存在。
diff --git a/docs/event-producer-consumer.i18n.yaml b/docs/event-producer-consumer.i18n.yaml
index aa70f43588..3f64be1349 100644
--- a/docs/event-producer-consumer.i18n.yaml
+++ b/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: 2281f3bbd07597f945c41ea04ff96dca60b8c2fd
-event-producer-consumer.zh.md: a615881f831503c2f8abcf23336f87f05640a87a
+event-producer-consumer.md: 50995814a2f4124f9d77d4c1689e28950476f81f
+event-producer-consumer.zh.md: 6b2c4738859b6526508f73b7aca5cedf27a3ee55
diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md
index 2281f3bbd0..50995814a2 100644
--- a/docs/event-producer-consumer.md
+++ b/docs/event-producer-consumer.md
@@ -7,7 +7,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| Event | Mode | Declared in | Dispatchers | Listeners |
| --- | --- | --- | --- | --- |
-| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:247`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
+| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:238`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:59`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` |
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
@@ -26,6 +26,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:610`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:589`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:596`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:43`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) |
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:89`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |
@@ -50,7 +51,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `hmr/reload` | `emit` | [`packages/boot/hmr/src/index.ts:35`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - |
| `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:73`](../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` |
+| `permission-presets/catalog-changed` | `emit` | [`packages/interaction/permission-presets/src/types.ts:48`](../packages/interaction/permission-presets/src/types.ts) | [`permission-presets`](../packages/interaction/permission-presets) (`events.dispatch`) | `remotes` |
| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:228`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:234`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:241`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
@@ -59,8 +60,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:63`](../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), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:75`](../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-preset-registry`](../packages/preset/agent-preset-registry), `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), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:84`](../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: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) |
+| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:75`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`emit`) | `remotes` |
| `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:173`](../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:147`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
@@ -75,7 +75,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:190`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:198`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`tool-present`](../packages/deliverables/tool-present) |
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:88`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` |
-| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:34`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `ui-sidebar-documentpreview` |
+| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:34`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `ui-sidebar-documentpreview`, `ui-theme` |
| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
| `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:68`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
| `workflow/end` | `emit` | [`packages/workflow/workflow/src/index.ts:89`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`workflow`](../packages/workflow/workflow) |
@@ -87,11 +87,13 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| Event string | Dispatchers | Listeners |
| --- | --- | --- |
+| `internal/config` | [`config-editor`](../packages/boot/config-editor) (`waterfall`) | [`llm-pi-ai`](../packages/llm/llm-pi-ai) |
| `internal/dispatch` | - | `agent-team`, [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), `ui-renderer`, [`user-approval`](../packages/interaction/user-approval), [`webhook`](../packages/webhook/webhook), [`workflow`](../packages/workflow/workflow) |
| `internal/plugin` | - | `computer-use-cua-driver-native`, `inspector`, `loader`, [`lsp-stdio`](../packages/lsp/lsp-stdio), [`mcp-client`](../packages/mcp/mcp-client), `modules` |
| `internal/service` | - | [`agent-preset-registry`](../packages/preset/agent-preset-registry), `gateway` |
| `internal/status` | - | [`agent`](../packages/core/agent), `inspector`, [`web`](../packages/web/web) |
| `internal/update` | - | [`app-boot`](../packages/boot/app-boot) |
+| `loader/volatile-update` | - | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), `speech-to-text` |
| `slots/changed` | `ui-renderer` (`emit`) | - |
Maintenance mode: generated: Cordis event declarations and producer/listener edges are resolved from the repository TypeScript Program.
diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md
index a615881f83..6b2c473885 100644
--- a/docs/event-producer-consumer.zh.md
+++ b/docs/event-producer-consumer.zh.md
@@ -9,7 +9,7 @@
| Event | Mode | Declared in | Dispatchers | Listeners |
| --- | --- | --- | --- | --- |
-| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:247`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
+| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:238`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:59`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` |
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
@@ -28,6 +28,7 @@
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:610`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:589`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:596`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
+| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:43`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) |
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:89`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |
@@ -43,16 +44,16 @@
| `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:90`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`credentials`](../packages/credentials/credentials), `remotes` |
| `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace), `workspace-controller` |
| `feedback/committed` | `parallel` | [`packages/feedback/message-feedback/src/index.ts:58`](../packages/feedback/message-feedback/src/index.ts) | [`message-feedback`](../packages/feedback/message-feedback) (`parallel`) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) |
-| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:67`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
-| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:77`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem), `workspace-files` |
-| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:59`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
+| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:66`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
+| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem), `workspace-files` |
+| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
| `goal/activation-changed` | `emit` | [`packages/goal/goal/src/types.ts:150`](../packages/goal/goal/src/types.ts) | [`goal`](../packages/goal/goal) (`emit`) | `remotes` |
| `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
| `hmr/change` | `emit` | [`packages/boot/hmr/src/index.ts:30`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - |
| `hmr/reload` | `emit` | [`packages/boot/hmr/src/index.ts:35`](../packages/boot/hmr/src/index.ts) | [`hmr`](../packages/boot/hmr) (`emit`) | - |
| `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:73`](../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` |
+| `permission-presets/catalog-changed` | `emit` | [`packages/interaction/permission-presets/src/types.ts:48`](../packages/interaction/permission-presets/src/types.ts) | [`permission-presets`](../packages/interaction/permission-presets) (`events.dispatch`) | `remotes` |
| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:228`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:234`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:241`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
@@ -61,8 +62,7 @@
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:63`](../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), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `session/event` | `emit` | [`packages/core/session/src/index.ts:75`](../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-preset-registry`](../packages/preset/agent-preset-registry), `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), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:84`](../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: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) |
+| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:75`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`emit`) | `remotes` |
| `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:173`](../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:147`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
@@ -89,11 +89,13 @@
| Event string | Dispatchers | Listeners |
| --- | --- | --- |
+| `internal/config` | [`config-editor`](../packages/boot/config-editor) (`waterfall`) | [`llm-pi-ai`](../packages/llm/llm-pi-ai) |
| `internal/dispatch` | - | `agent-team`, [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), `ui-renderer`, [`user-approval`](../packages/interaction/user-approval), [`webhook`](../packages/webhook/webhook), [`workflow`](../packages/workflow/workflow) |
| `internal/plugin` | - | `computer-use-cua-driver-native`, `inspector`, `loader`, [`lsp-stdio`](../packages/lsp/lsp-stdio), [`mcp-client`](../packages/mcp/mcp-client), `modules` |
| `internal/service` | - | [`agent-preset-registry`](../packages/preset/agent-preset-registry), `gateway` |
| `internal/status` | - | [`agent`](../packages/core/agent), `inspector`, [`web`](../packages/web/web) |
| `internal/update` | - | [`app-boot`](../packages/boot/app-boot) |
+| `loader/volatile-update` | - | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai) |
| `slots/changed` | `ui-renderer` (`emit`) | - |
Maintenance mode: generated: Cordis event declarations and producer/listener edges are resolved from the repository TypeScript Program.
diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml
index b3adeb8c42..235cd3fdef 100644
--- a/docs/module-graph.i18n.yaml
+++ b/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: ae55f2bb2cf29f0f6f4ad3c16df9abb7a3511191
-module-graph.zh.md: cef0a09afb07c4f8ee5392899a1cee3f9cadf3ca
+module-graph.md: e36487734f39545fee9b188d9443336ad7ed2e0e
+module-graph.zh.md: 646aa21ea348ddf5e73d12b94247319cfe3768d6
diff --git a/docs/module-graph.md b/docs/module-graph.md
index ae55f2bb2c..e36487734f 100644
--- a/docs/module-graph.md
+++ b/docs/module-graph.md
@@ -129,6 +129,7 @@ flowchart TD
subgraph group_boot["packages/boot"]
pkg_app_boot["app-boot"]
pkg_cmdline["cmdline"]
+ pkg_config_editor["config-editor"]
pkg_hmr["hmr"]
pkg_plugin_manager["plugin-manager"]
end
@@ -354,7 +355,6 @@ flowchart TD
end
subgraph group_settings["packages/settings"]
pkg_settings["settings"]
- pkg_settings_file["settings-file"]
end
subgraph group_shell["packages/shell"]
pkg_bash_local["bash-local"]
@@ -535,11 +535,7 @@ flowchart TD
pkg_session_turn_outline --> pkg_llm
pkg_session_turn_outline --> pkg_session
pkg_session_turn_outline --> pkg_session_projection
- pkg_settings_file --> pkg_atomic_write
- pkg_settings_file --> pkg_home_paths
- pkg_settings_file --> pkg_settings
pkg_shell --> pkg_sandbox
- pkg_shell --> pkg_settings
pkg_shell --> pkg_subprocess
pkg_workspace --> pkg_invariants
pkg_workspace --> pkg_session
@@ -556,7 +552,6 @@ flowchart TD
pkg_llm_deepseek --> pkg_home_paths
pkg_llm_deepseek --> pkg_launch_environment
pkg_llm_deepseek --> pkg_llm
- pkg_llm_deepseek --> pkg_settings
pkg_llm_deepseek --> pkg_timeout
pkg_llm_pi_ai --> pkg_attachment
pkg_llm_pi_ai --> pkg_authorization
@@ -564,7 +559,6 @@ flowchart TD
pkg_llm_pi_ai --> pkg_fs
pkg_llm_pi_ai --> pkg_launch_environment
pkg_llm_pi_ai --> pkg_llm
- pkg_llm_pi_ai --> pkg_settings
pkg_llm_pi_ai --> pkg_timeout
pkg_llm_retry --> pkg_agent
pkg_llm_retry --> pkg_brand
@@ -575,7 +569,6 @@ flowchart TD
pkg_llm_retry --> pkg_timeout
pkg_agent_default_model --> pkg_agent
pkg_agent_default_model --> pkg_llm
- pkg_agent_default_model --> pkg_settings
pkg_goal --> pkg_agent
pkg_goal --> pkg_brand
pkg_goal --> pkg_invariants
@@ -593,7 +586,6 @@ flowchart TD
pkg_web_search_deepseek --> pkg_credentials
pkg_web_search_deepseek --> pkg_launch_environment
pkg_web_search_deepseek --> pkg_session
- pkg_web_search_deepseek --> pkg_settings
pkg_web_search_deepseek --> pkg_web
pkg_hook_protocol --> pkg_invariants
pkg_hook_protocol --> pkg_session
@@ -605,6 +597,8 @@ flowchart TD
pkg_api_workspace_controller --> pkg_storage_domain
pkg_api_workspace_controller --> pkg_typert_protocol
pkg_api_workspace_controller --> pkg_workspace
+ pkg_config_editor --> pkg_app_boot
+ pkg_config_editor --> pkg_hmr
pkg_file_reference --> pkg_agent
pkg_time_context --> pkg_agent
pkg_time_context --> pkg_invariants
@@ -664,11 +658,9 @@ flowchart TD
pkg_session_title --> pkg_llm
pkg_session_title --> pkg_session
pkg_session_title --> pkg_session_projection
- pkg_bash_local --> pkg_settings
pkg_bash_local --> pkg_shell
pkg_bash_local --> pkg_subprocess
pkg_bash_local --> pkg_timeout
- pkg_pwsh_local --> pkg_settings
pkg_pwsh_local --> pkg_shell
pkg_pwsh_local --> pkg_subprocess
pkg_pwsh_local --> pkg_timeout
@@ -720,7 +712,6 @@ flowchart TD
pkg_permission_presets --> pkg_sandbox_policy
pkg_permission_presets --> pkg_session
pkg_permission_presets --> pkg_session_projection
- pkg_permission_presets --> pkg_settings
pkg_permission_presets --> pkg_shell
pkg_permission_presets --> pkg_typert_protocol
pkg_permission_presets --> pkg_user_approval
@@ -761,7 +752,6 @@ flowchart TD
pkg_agent_loop --> pkg_session
pkg_agent_loop --> pkg_session_persistence
pkg_agent_loop --> pkg_session_projection
- pkg_agent_loop --> pkg_settings
pkg_agent_loop --> pkg_system_prompt
pkg_agent_loop --> pkg_tools
pkg_agent_tool_presentation --> pkg_tools
@@ -1088,7 +1078,6 @@ flowchart TD
pkg_subagent --> pkg_session_projection
pkg_subagent --> pkg_session_projection_cache
pkg_subagent --> pkg_session_query
- pkg_subagent --> pkg_settings
pkg_subagent --> pkg_system_prompt
pkg_subagent --> pkg_tools
pkg_subagent --> pkg_typert_protocol
@@ -1171,7 +1160,6 @@ flowchart TD
pkg_tool_subagent --> pkg_scope
pkg_tool_subagent --> pkg_session
pkg_tool_subagent --> pkg_session_projection
- pkg_tool_subagent --> pkg_settings
pkg_tool_subagent --> pkg_subagent
pkg_tool_subagent --> pkg_system_prompt
pkg_tool_subagent --> pkg_tools
@@ -1501,20 +1489,20 @@ flowchart TD
| [`session-projection-cache`](../packages/session/session-projection-cache) | `session` | [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain) |
| [`session-stats`](../packages/session/session-stats) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
| [`session-turn-outline`](../packages/session/session-turn-outline) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
-| [`settings-file`](../packages/settings/settings-file) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`settings`](../packages/settings/settings) |
-| [`shell`](../packages/shell/shell) | `shell` | [`sandbox`](../packages/sandbox/sandbox), [`settings`](../packages/settings/settings), [`subprocess`](../packages/subprocess/subprocess) |
+| [`shell`](../packages/shell/shell) | `shell` | [`sandbox`](../packages/sandbox/sandbox), [`subprocess`](../packages/subprocess/subprocess) |
| [`workspace`](../packages/workspace/workspace) | `workspace` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage`](../packages/storage/storage), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
-| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`atomic-write`](../packages/util/atomic-write), [`attachment`](../packages/attachment/attachment), [`credentials`](../packages/credentials/credentials), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
-| [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`attachment`](../packages/attachment/attachment), [`authorization`](../packages/credentials/authorization), [`credentials`](../packages/credentials/credentials), [`fs`](../packages/fs/fs), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
+| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`atomic-write`](../packages/util/atomic-write), [`attachment`](../packages/attachment/attachment), [`credentials`](../packages/credentials/credentials), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout) |
+| [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`attachment`](../packages/attachment/attachment), [`authorization`](../packages/credentials/authorization), [`credentials`](../packages/credentials/credentials), [`fs`](../packages/fs/fs), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout) |
| [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`timeout`](../packages/util/timeout) |
-| [`agent-default-model`](../packages/core/agent-default-model) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings) |
+| [`agent-default-model`](../packages/core/agent-default-model) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm) |
| [`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), [`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) |
+| [`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), [`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) |
+| [`config-editor`](../packages/boot/config-editor) | `boot` | [`app-boot`](../packages/boot/app-boot), [`hmr`](../packages/boot/hmr) |
| [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent) |
| [`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) |
@@ -1529,8 +1517,8 @@ flowchart TD
| [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) |
| [`session-telemetry`](../packages/session/session-telemetry) | `session` | [`agent`](../packages/core/agent), [`session`](../packages/core/session) |
| [`session-title`](../packages/session/session-title) | `session` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
-| [`bash-local`](../packages/shell/bash-local) | `shell` | [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
-| [`pwsh-local`](../packages/shell/pwsh-local) | `shell` | [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`bash-local`](../packages/shell/bash-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`pwsh-local`](../packages/shell/pwsh-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand) |
| [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`http-proxy`](../packages/util/http-proxy), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
@@ -1540,14 +1528,14 @@ flowchart TD
| [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
| [`compaction`](../packages/compaction/compaction) | `compaction` | [`brand`](../packages/util/brand), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`commands`](../packages/interaction/commands), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
-| [`permission-presets`](../packages/interaction/permission-presets) | `interaction` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) |
+| [`permission-presets`](../packages/interaction/permission-presets) | `interaction` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) |
| [`jobs-local`](../packages/jobs/jobs-local) | `jobs` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`scope`](../packages/core/scope), [`timeout`](../packages/util/timeout) |
| [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node) | `ptc-runtime` | [`fs`](../packages/fs/fs), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`session-title-llm`](../packages/session/session-title-llm) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`timeout`](../packages/util/timeout) |
| [`bash-sandbox`](../packages/shell/bash-sandbox) | `shell` | [`bash-local`](../packages/shell/bash-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell) |
| [`pwsh-sandbox`](../packages/shell/pwsh-sandbox) | `shell` | [`pwsh-local`](../packages/shell/pwsh-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell) |
| [`terminal-bash`](../packages/terminal/terminal-bash) | `terminal` | [`agent`](../packages/core/agent), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subprocess`](../packages/subprocess/subprocess), [`terminal`](../packages/terminal/terminal) |
-| [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
+| [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`agent-tool-presentation`](../packages/core/agent-tool-presentation) | `core` | [`tools`](../packages/core/tools) |
| [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
@@ -1608,7 +1596,7 @@ flowchart TD
| [`subprocess-ssh`](../packages/ssh/subprocess-ssh) | `ssh` | [`ssh`](../packages/ssh/ssh), [`subprocess`](../packages/subprocess/subprocess), [`subprocess-local`](../packages/subprocess/subprocess-local) |
| [`agent-loop-testkit`](../packages/test-support/agent-loop-testkit) | `test-support` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`webhook`](../packages/webhook/webhook) | `webhook` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) |
-| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`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-query`](../packages/session-query/session-query), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time) |
+| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`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-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time) |
| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) |
@@ -1621,7 +1609,7 @@ flowchart TD
| [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
-| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
+| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-file-upload`](../packages/client/file-upload), [`commands`](../packages/interaction/commands), [`file-reference`](../packages/context/file-reference), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`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-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-values`](../packages/util/values), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) |
diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md
index cef0a09afb..646aa21ea3 100644
--- a/docs/module-graph.zh.md
+++ b/docs/module-graph.zh.md
@@ -131,6 +131,7 @@ flowchart TD
subgraph group_boot["packages/boot"]
pkg_app_boot["app-boot"]
pkg_cmdline["cmdline"]
+ pkg_config_editor["config-editor"]
pkg_hmr["hmr"]
pkg_plugin_manager["plugin-manager"]
end
@@ -356,7 +357,6 @@ flowchart TD
end
subgraph group_settings["packages/settings"]
pkg_settings["settings"]
- pkg_settings_file["settings-file"]
end
subgraph group_shell["packages/shell"]
pkg_bash_local["bash-local"]
@@ -537,11 +537,7 @@ flowchart TD
pkg_session_turn_outline --> pkg_llm
pkg_session_turn_outline --> pkg_session
pkg_session_turn_outline --> pkg_session_projection
- pkg_settings_file --> pkg_atomic_write
- pkg_settings_file --> pkg_home_paths
- pkg_settings_file --> pkg_settings
pkg_shell --> pkg_sandbox
- pkg_shell --> pkg_settings
pkg_shell --> pkg_subprocess
pkg_workspace --> pkg_invariants
pkg_workspace --> pkg_session
@@ -558,7 +554,6 @@ flowchart TD
pkg_llm_deepseek --> pkg_home_paths
pkg_llm_deepseek --> pkg_launch_environment
pkg_llm_deepseek --> pkg_llm
- pkg_llm_deepseek --> pkg_settings
pkg_llm_deepseek --> pkg_timeout
pkg_llm_pi_ai --> pkg_attachment
pkg_llm_pi_ai --> pkg_authorization
@@ -566,7 +561,6 @@ flowchart TD
pkg_llm_pi_ai --> pkg_fs
pkg_llm_pi_ai --> pkg_launch_environment
pkg_llm_pi_ai --> pkg_llm
- pkg_llm_pi_ai --> pkg_settings
pkg_llm_pi_ai --> pkg_timeout
pkg_llm_retry --> pkg_agent
pkg_llm_retry --> pkg_brand
@@ -577,7 +571,6 @@ flowchart TD
pkg_llm_retry --> pkg_timeout
pkg_agent_default_model --> pkg_agent
pkg_agent_default_model --> pkg_llm
- pkg_agent_default_model --> pkg_settings
pkg_goal --> pkg_agent
pkg_goal --> pkg_brand
pkg_goal --> pkg_invariants
@@ -595,7 +588,6 @@ flowchart TD
pkg_web_search_deepseek --> pkg_credentials
pkg_web_search_deepseek --> pkg_launch_environment
pkg_web_search_deepseek --> pkg_session
- pkg_web_search_deepseek --> pkg_settings
pkg_web_search_deepseek --> pkg_web
pkg_hook_protocol --> pkg_invariants
pkg_hook_protocol --> pkg_session
@@ -607,6 +599,8 @@ flowchart TD
pkg_api_workspace_controller --> pkg_storage_domain
pkg_api_workspace_controller --> pkg_typert_protocol
pkg_api_workspace_controller --> pkg_workspace
+ pkg_config_editor --> pkg_app_boot
+ pkg_config_editor --> pkg_hmr
pkg_file_reference --> pkg_agent
pkg_time_context --> pkg_agent
pkg_time_context --> pkg_invariants
@@ -666,11 +660,9 @@ flowchart TD
pkg_session_title --> pkg_llm
pkg_session_title --> pkg_session
pkg_session_title --> pkg_session_projection
- pkg_bash_local --> pkg_settings
pkg_bash_local --> pkg_shell
pkg_bash_local --> pkg_subprocess
pkg_bash_local --> pkg_timeout
- pkg_pwsh_local --> pkg_settings
pkg_pwsh_local --> pkg_shell
pkg_pwsh_local --> pkg_subprocess
pkg_pwsh_local --> pkg_timeout
@@ -722,7 +714,6 @@ flowchart TD
pkg_permission_presets --> pkg_sandbox_policy
pkg_permission_presets --> pkg_session
pkg_permission_presets --> pkg_session_projection
- pkg_permission_presets --> pkg_settings
pkg_permission_presets --> pkg_shell
pkg_permission_presets --> pkg_typert_protocol
pkg_permission_presets --> pkg_user_approval
@@ -763,7 +754,6 @@ flowchart TD
pkg_agent_loop --> pkg_session
pkg_agent_loop --> pkg_session_persistence
pkg_agent_loop --> pkg_session_projection
- pkg_agent_loop --> pkg_settings
pkg_agent_loop --> pkg_system_prompt
pkg_agent_loop --> pkg_tools
pkg_agent_tool_presentation --> pkg_tools
@@ -1090,7 +1080,6 @@ flowchart TD
pkg_subagent --> pkg_session_projection
pkg_subagent --> pkg_session_projection_cache
pkg_subagent --> pkg_session_query
- pkg_subagent --> pkg_settings
pkg_subagent --> pkg_system_prompt
pkg_subagent --> pkg_tools
pkg_subagent --> pkg_typert_protocol
@@ -1173,7 +1162,6 @@ flowchart TD
pkg_tool_subagent --> pkg_scope
pkg_tool_subagent --> pkg_session
pkg_tool_subagent --> pkg_session_projection
- pkg_tool_subagent --> pkg_settings
pkg_tool_subagent --> pkg_subagent
pkg_tool_subagent --> pkg_system_prompt
pkg_tool_subagent --> pkg_tools
@@ -1503,20 +1491,20 @@ flowchart TD
| [`session-projection-cache`](../packages/session/session-projection-cache) | `session` | [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain) |
| [`session-stats`](../packages/session/session-stats) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
| [`session-turn-outline`](../packages/session/session-turn-outline) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
-| [`settings-file`](../packages/settings/settings-file) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`settings`](../packages/settings/settings) |
-| [`shell`](../packages/shell/shell) | `shell` | [`sandbox`](../packages/sandbox/sandbox), [`settings`](../packages/settings/settings), [`subprocess`](../packages/subprocess/subprocess) |
+| [`shell`](../packages/shell/shell) | `shell` | [`sandbox`](../packages/sandbox/sandbox), [`subprocess`](../packages/subprocess/subprocess) |
| [`workspace`](../packages/workspace/workspace) | `workspace` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage`](../packages/storage/storage), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
-| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`atomic-write`](../packages/util/atomic-write), [`attachment`](../packages/attachment/attachment), [`credentials`](../packages/credentials/credentials), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
-| [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`attachment`](../packages/attachment/attachment), [`authorization`](../packages/credentials/authorization), [`credentials`](../packages/credentials/credentials), [`fs`](../packages/fs/fs), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
+| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`atomic-write`](../packages/util/atomic-write), [`attachment`](../packages/attachment/attachment), [`credentials`](../packages/credentials/credentials), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout) |
+| [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`attachment`](../packages/attachment/attachment), [`authorization`](../packages/credentials/authorization), [`credentials`](../packages/credentials/credentials), [`fs`](../packages/fs/fs), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`timeout`](../packages/util/timeout) |
| [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`timeout`](../packages/util/timeout) |
-| [`agent-default-model`](../packages/core/agent-default-model) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings) |
+| [`agent-default-model`](../packages/core/agent-default-model) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm) |
| [`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), [`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) |
+| [`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), [`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) |
+| [`config-editor`](../packages/boot/config-editor) | `boot` | [`app-boot`](../packages/boot/app-boot), [`hmr`](../packages/boot/hmr) |
| [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent) |
| [`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) |
@@ -1531,8 +1519,8 @@ flowchart TD
| [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt) |
| [`session-telemetry`](../packages/session/session-telemetry) | `session` | [`agent`](../packages/core/agent), [`session`](../packages/core/session) |
| [`session-title`](../packages/session/session-title) | `session` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
-| [`bash-local`](../packages/shell/bash-local) | `shell` | [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
-| [`pwsh-local`](../packages/shell/pwsh-local) | `shell` | [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`bash-local`](../packages/shell/bash-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
+| [`pwsh-local`](../packages/shell/pwsh-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`terminal`](../packages/terminal/terminal) | `terminal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand) |
| [`loader-smoke`](../packages/test-support/loader-smoke) | `test-support` | [`agent`](../packages/core/agent), [`http-proxy`](../packages/util/http-proxy), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
@@ -1542,14 +1530,14 @@ flowchart TD
| [`fs-sandbox`](../packages/fs/fs-sandbox) | `fs` | [`fs`](../packages/fs/fs), [`fs-local`](../packages/fs/fs-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy) |
| [`compaction`](../packages/compaction/compaction) | `compaction` | [`brand`](../packages/util/brand), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`command-feedback`](../packages/feedback/command-feedback) | `feedback` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`commands`](../packages/interaction/commands), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
-| [`permission-presets`](../packages/interaction/permission-presets) | `interaction` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`shell`](../packages/shell/shell), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) |
+| [`permission-presets`](../packages/interaction/permission-presets) | `interaction` | [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) |
| [`jobs-local`](../packages/jobs/jobs-local) | `jobs` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`scope`](../packages/core/scope), [`timeout`](../packages/util/timeout) |
| [`ptc-runtime-node`](../packages/ptc-runtime/ptc-runtime-node) | `ptc-runtime` | [`fs`](../packages/fs/fs), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`session-title-llm`](../packages/session/session-title-llm) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`timeout`](../packages/util/timeout) |
| [`bash-sandbox`](../packages/shell/bash-sandbox) | `shell` | [`bash-local`](../packages/shell/bash-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell) |
| [`pwsh-sandbox`](../packages/shell/pwsh-sandbox) | `shell` | [`pwsh-local`](../packages/shell/pwsh-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell) |
| [`terminal-bash`](../packages/terminal/terminal-bash) | `terminal` | [`agent`](../packages/core/agent), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subprocess`](../packages/subprocess/subprocess), [`terminal`](../packages/terminal/terminal) |
-| [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
+| [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`agent-tool-presentation`](../packages/core/agent-tool-presentation) | `core` | [`tools`](../packages/core/tools) |
| [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
@@ -1610,7 +1598,7 @@ flowchart TD
| [`subprocess-ssh`](../packages/ssh/subprocess-ssh) | `ssh` | [`ssh`](../packages/ssh/ssh), [`subprocess`](../packages/subprocess/subprocess), [`subprocess-local`](../packages/subprocess/subprocess-local) |
| [`agent-loop-testkit`](../packages/test-support/agent-loop-testkit) | `test-support` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`webhook`](../packages/webhook/webhook) | `webhook` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) |
-| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`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-query`](../packages/session-query/session-query), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time) |
+| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`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-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time) |
| [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
| [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) |
@@ -1623,7 +1611,7 @@ flowchart TD
| [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
-| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`settings`](../packages/settings/settings), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
+| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-file-upload`](../packages/client/file-upload), [`commands`](../packages/interaction/commands), [`file-reference`](../packages/context/file-reference), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`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-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-values`](../packages/util/values), [`util-workspace-path`](../packages/util/workspace-path), [`workspace`](../packages/workspace/workspace) |
diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml
index 42b628640b..e9d408296b 100644
--- a/docs/persistence-catalog.i18n.yaml
+++ b/docs/persistence-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/persistence-catalog.md
-persistence-catalog.md: 30fc6dc277bdbdaf5330740a8a291101358dbaa2
-persistence-catalog.zh.md: fff627b8ff3a8c1c97f7793b0abca7008f395eb0
+persistence-catalog.md: d771378c6899d2c55c30407a9e87ebe3689f9838
+persistence-catalog.zh.md: 2ca7c9940efdbeb8309010891025d5bd73030da3
diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md
index 30fc6dc277..d771378c68 100644
--- a/docs/persistence-catalog.md
+++ b/docs/persistence-catalog.md
@@ -680,7 +680,7 @@ Source: [`packages/api/session-controller/src/types.ts:40`](../packages/api/sess
'permission/preset': { preset: string }
```
-Source: [`packages/interaction/permission-presets/src/index.ts:57`](../packages/interaction/permission-presets/src/index.ts)
+Source: [`packages/interaction/permission-presets/src/index.ts:59`](../packages/interaction/permission-presets/src/index.ts)
### `plan/*`
@@ -7456,7 +7456,7 @@ Sources: [`packages/interaction/user-approval/src/index.ts:40`](../packages/inte
SHA-256: `8a9c990773f798eac10ca8878e6bcc531ee080bc589cac956730e462cc6a09ad`
-Sources: [`packages/interaction/permission-presets/src/index.ts:57`](../packages/interaction/permission-presets/src/index.ts)
+Sources: [`packages/interaction/permission-presets/src/index.ts:59`](../packages/interaction/permission-presets/src/index.ts)
| Property | Presence | Type |
|---|---|---|
diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md
index fff627b8ff..2ca7c9940e 100644
--- a/docs/persistence-catalog.zh.md
+++ b/docs/persistence-catalog.zh.md
@@ -682,7 +682,7 @@ export type SessionEvent = {
'permission/preset': { preset: string }
```
-来源:[`packages/interaction/permission-presets/src/index.ts:57`](../packages/interaction/permission-presets/src/index.ts)
+来源:[`packages/interaction/permission-presets/src/index.ts:59`](../packages/interaction/permission-presets/src/index.ts)
### `plan/*`
@@ -7458,7 +7458,7 @@ SHA-256: `11f70b595aa76897fc4e25a352bfa718c659f44b799e5aa14e84e5a38fe72912`
SHA-256: `8a9c990773f798eac10ca8878e6bcc531ee080bc589cac956730e462cc6a09ad`
-来源:[`packages/interaction/permission-presets/src/index.ts:57`](../packages/interaction/permission-presets/src/index.ts)
+来源:[`packages/interaction/permission-presets/src/index.ts:59`](../packages/interaction/permission-presets/src/index.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
diff --git a/docs/persistence-schema.json b/docs/persistence-schema.json
index df74feb6a1..1164995a46 100644
--- a/docs/persistence-schema.json
+++ b/docs/persistence-schema.json
@@ -63338,7 +63338,7 @@
},
"names": [],
"sources": [
- "packages/interaction/permission-presets/src/index.ts:57"
+ "packages/interaction/permission-presets/src/index.ts:59"
]
},
{
diff --git a/docs/subsystems/boot.i18n.yaml b/docs/subsystems/boot.i18n.yaml
index c602a72982..c5d46dd2cb 100644
--- a/docs/subsystems/boot.i18n.yaml
+++ b/docs/subsystems/boot.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/boot.md
-boot.md: d8f7ea8018dbbf5ec3b20f14cf4c1884af167a94
-boot.zh.md: 3353427b4e727713113842b89e094f26e75217bb
+boot.md: a52c362aef1c61f9087da7c1abfce8d4671d9e92
+boot.zh.md: 5933e206127bd5ca4aa0b8888cf81f0e1aeb9eb7
diff --git a/docs/subsystems/boot.md b/docs/subsystems/boot.md
index d8f7ea8018..a52c362aef 100644
--- a/docs/subsystems/boot.md
+++ b/docs/subsystems/boot.md
@@ -26,6 +26,33 @@ The [boot package group](../../packages/boot/README.md) owns launcher-provided p
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+
+
+### `ctx.configEditor` — `ConfigEditor`
+
+Persist complete raw configs and apply them through the normal Loader path.
+
+```ts cordis-catalog
+/** Addressable profile rows; nested Includes have independent configuration ownership.
+ * @returns Active entries with unique profile patch ids.
+ */
+entries(): Entry[]
+
+/** Read inherited and explicit profile values for the active entries.
+ * @returns Detached layer values alongside their Loader entries.
+ */
+configuration(): Array<{ entry: Entry; inherited: Record; override: Record }>
+
+/** Validate, persist, and reconcile a plugin's next config; ordinary fields keep normal lifecycle rules.
+ * @param entry Current Loader entry, also used to detect replacement during the write.
+ * @param change Derive a raw config from the current entry and its inherited layer.
+ * @returns Fulfillment after Loader reconciliation completes.
+ */
+async edit( entry: Entry, change: (current: Record, inherited: Record) => Record, ): Promise
+```
+
+Source: [`packages/boot/config-editor/src/index.ts`](../../packages/boot/config-editor/src/index.ts)
+
### `ctx.hmr` — `Hmr`
@@ -143,6 +170,27 @@ Current profile facts; scheduling and mutation belong to their callers.
Source: [`packages/boot/app-boot/src/profile-context.ts`](../../packages/boot/app-boot/src/profile-context.ts)
+
+
+### `app-boot/*` events
+
+
+
+#### `app-boot/config-reload` — emit
+
+Profile patches were reconciled into the running Loader tree: every entry update settled and no new inactive entry was introduced. Carries no diff; listeners re-read Loader entries.
+
+```ts cordis-catalog
+/**
+ * Profile patches were reconciled into the running Loader tree: every entry update settled and no new
+ * inactive entry was introduced. Carries no diff; listeners re-read Loader entries.
+ * @mode emit
+ */
+'app-boot/config-reload'(): void
+```
+
+Source: [`packages/boot/app-boot/src/index.ts`](../../packages/boot/app-boot/src/index.ts)
+
### `hmr/*` events
diff --git a/docs/subsystems/boot.zh.md b/docs/subsystems/boot.zh.md
index 3353427b4e..5933e20612 100644
--- a/docs/subsystems/boot.zh.md
+++ b/docs/subsystems/boot.zh.md
@@ -26,6 +26,33 @@
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.zh.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
+
+
+### `ctx.configEditor` — `ConfigEditor`
+
+Persist complete raw configs and apply them through the normal Loader path.
+
+```ts cordis-catalog
+/** Addressable profile rows; nested Includes have independent configuration ownership.
+ * @returns Active entries with unique profile patch ids.
+ */
+entries(): Entry[]
+
+/** Read inherited and explicit profile values for the active entries.
+ * @returns Detached layer values alongside their Loader entries.
+ */
+configuration(): Array<{ entry: Entry; inherited: Record; override: Record }>
+
+/** Validate, persist, and reconcile a plugin's next config; ordinary fields keep normal lifecycle rules.
+ * @param entry Current Loader entry, also used to detect replacement during the write.
+ * @param change Derive a raw config from the current entry and its inherited layer.
+ * @returns Fulfillment after Loader reconciliation completes.
+ */
+async edit( entry: Entry, change: (current: Record, inherited: Record) => Record, ): Promise
+```
+
+Source: [`packages/boot/config-editor/src/index.ts`](../../packages/boot/config-editor/src/index.ts)
+
### `ctx.hmr` — `Hmr`
@@ -143,6 +170,27 @@ Current profile facts; scheduling and mutation belong to their callers.
Source: [`packages/boot/app-boot/src/profile-context.ts`](../../packages/boot/app-boot/src/profile-context.ts)
+
+
+### `app-boot/*` events
+
+
+
+#### `app-boot/config-reload` — emit
+
+Profile patches were reconciled into the running Loader tree: every entry update settled and no new inactive entry was introduced. Carries no diff; listeners re-read Loader entries.
+
+```ts cordis-catalog
+/**
+ * Profile patches were reconciled into the running Loader tree: every entry update settled and no new
+ * inactive entry was introduced. Carries no diff; listeners re-read Loader entries.
+ * @mode emit
+ */
+'app-boot/config-reload'(): void
+```
+
+Source: [`packages/boot/app-boot/src/index.ts`](../../packages/boot/app-boot/src/index.ts)
+
### `hmr/*` events
diff --git a/docs/subsystems/core.i18n.yaml b/docs/subsystems/core.i18n.yaml
index 1c711f241b..29fbf9d0ae 100644
--- a/docs/subsystems/core.i18n.yaml
+++ b/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: fafe23cb473e30c74c17308f4ab79d982f609452
-core.zh.md: 5945720e38a89846ae14694dd7cf2b45af7433ad
+core.md: aa9af309c03ebedae353139c4017d19f2607dd7d
+core.zh.md: b18ff5bba1c0309e9b9dcdf1f3e97adb5895caf6
diff --git a/docs/subsystems/core.md b/docs/subsystems/core.md
index fafe23cb47..aa9af309c0 100644
--- a/docs/subsystems/core.md
+++ b/docs/subsystems/core.md
@@ -421,7 +421,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp
### `ctx.agentDefaultModel` — `AgentDefaultModelConfig`
-Owns the default model selection independently of any Host or transport. The composition entry remains usable without a settings provider; when one is mounted, its user layer is read live.
+Owns the default model selection independently of any Host or transport. Each operation reads the owning Config references.
```ts cordis-catalog
/**
@@ -431,10 +431,10 @@ Owns the default model selection independently of any Host or transport. The com
currentSelection(): ModelSelection
/**
- * Save the complete default model selection. A deployment without a settings
- * provider keeps its composition entry.
+ * Save the complete default model selection. A deployment without a configuration
+ * editor keeps its composition entry.
* @param next - resolved selection accepted by an entry point.
- * @returns fulfillment after the optional settings write settles.
+ * @returns fulfillment after the optional profile write settles.
*/
async saveSelection(next: ModelSelection): Promise
```
diff --git a/docs/subsystems/core.zh.md b/docs/subsystems/core.zh.md
index 5945720e38..b18ff5bba1 100644
--- a/docs/subsystems/core.zh.md
+++ b/docs/subsystems/core.zh.md
@@ -431,7 +431,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp
### `ctx.agentDefaultModel` — `AgentDefaultModelConfig`
-Owns the default model selection independently of any Host or transport. The composition entry remains usable without a settings provider; when one is mounted, its user layer is read live.
+Owns the default model selection independently of any Host or transport. Each operation reads the owning Config references.
```ts cordis-catalog
/**
@@ -441,10 +441,10 @@ Owns the default model selection independently of any Host or transport. The com
currentSelection(): ModelSelection
/**
- * Save the complete default model selection. A deployment without a settings
- * provider keeps its composition entry.
+ * Save the complete default model selection. A deployment without a configuration
+ * editor keeps its composition entry.
* @param next - resolved selection accepted by an entry point.
- * @returns fulfillment after the optional settings write settles.
+ * @returns fulfillment after the optional profile write settles.
*/
async saveSelection(next: ModelSelection): Promise
```
diff --git a/docs/subsystems/permission-presets.i18n.yaml b/docs/subsystems/permission-presets.i18n.yaml
index ccc8f24edd..f735d8ebd1 100644
--- a/docs/subsystems/permission-presets.i18n.yaml
+++ b/docs/subsystems/permission-presets.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/permission-presets.md
-permission-presets.md: 3414678e1e2cba600cd81fd1cfa4b8f3df1aa839
-permission-presets.zh.md: e3e2d5bc99432f5af2473e904b085b2484e49226
+permission-presets.md: d843fab9493d9dda8fb21f8f859c4bac12dabdfd
+permission-presets.zh.md: f310363cdda1899a972850df3bf4200693df42a5
diff --git a/docs/subsystems/permission-presets.md b/docs/subsystems/permission-presets.md
index 3414678e1e..d843fab949 100644
--- a/docs/subsystems/permission-presets.md
+++ b/docs/subsystems/permission-presets.md
@@ -33,12 +33,12 @@ interface Config {
* never). The names `custom` and `auto` are reserved for derived state and
* the Auto review integration respectively.
*/
- presets?: Record
+ presets: Record
/**
* Default for new sessions. When omitted, the preset matching the composed
* sandbox and approval defaults is used.
*/
- defaultPreset?: string
+ defaultPreset: Volatile
}
```
diff --git a/docs/subsystems/permission-presets.zh.md b/docs/subsystems/permission-presets.zh.md
index e3e2d5bc99..f310363cdd 100644
--- a/docs/subsystems/permission-presets.zh.md
+++ b/docs/subsystems/permission-presets.zh.md
@@ -33,12 +33,12 @@ interface Config {
* never). The names `custom` and `auto` are reserved for derived state and
* the Auto review integration respectively.
*/
- presets?: Record
+ presets: Record
/**
* Default for new sessions. When omitted, the preset matching the composed
* sandbox and approval defaults is used.
*/
- defaultPreset?: string
+ defaultPreset: Volatile
}
```
diff --git a/docs/subsystems/settings.i18n.yaml b/docs/subsystems/settings.i18n.yaml
index 22a45d5931..664f17aa15 100644
--- a/docs/subsystems/settings.i18n.yaml
+++ b/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: 875b25f58dea82a2a69f2d4efa7696bdb39c257d
-settings.zh.md: 88c50e5f991ec5b59a1f5b2ce3fc98c1bb84936d
+settings.md: ff047b74149a8d7a1f4471843de82f5c45b7fa6b
+settings.zh.md: cb2fbeb69444036875954001f464f5fdcd6646bb
diff --git a/docs/subsystems/settings.md b/docs/subsystems/settings.md
index 875b25f58d..ff047b7414 100644
--- a/docs/subsystems/settings.md
+++ b/docs/subsystems/settings.md
@@ -1,169 +1,18 @@
-# User Settings
+# Plugin Configuration Forms
English | [中文](settings.zh.md)
-The user-settings seam of [dsh-settings](../../packages/settings/settings) holds one user-owned document of per-namespace sections and resolves each registered namespace as schema defaults, then the registrant's composition `base`, then the user section. Providers such as [dsh-settings-file](../../packages/settings/settings-file) store the raw document and push external edits; consumer plugins register a schema and read or observe the resolved value. Composition config stays in `cordis.yml` — a namespace carries only the user-editable subset.
+The [settings service](../../packages/settings/settings/README.md) projects volatile Config fields from active profile entries. The [configuration editor](../../packages/boot/config-editor/README.md) persists edits through Cordis patches. Business consumers read `.get()` on their own Config references.
-Source: [`packages/settings/settings/src/index.ts`](../../packages/settings/settings/src/index.ts)
+## Identity and values
-## Identity
+A form namespace is the local id of a uniquely addressed entry in the active profile. Multiple plugin instances have separate forms when their entry ids differ. Ordinary fields are excluded. Descriptors carry resolved values, inherited values, explicit profile overrides, and an optimistic revision.
-A namespace names one plugin-owned section of the user document. The brand prevents callers from mixing settings namespaces with other ids passed between packages or processes; construction validates lowercase kebab-case syntax.
+## Edits
-```ts type-equiv
-/** Nominal id of one registered settings namespace. */
-type SettingsNamespace = Branded<'SettingsNamespace'>
-```
+`update` merges submitted fields. `replace` resets live fields to inherited configuration before applying submitted fields. `mutate` addresses individual paths, preserving secrets absent from a client response. Every write validates the complete Config and refuses stale revisions before persistence.
-## 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.
-
-```ts type-equiv
-/** Registration options beyond the namespace schema. */
-interface SettingsRegisterOptions {
- /** Composition-layer values resolved below the user layer (entry-config subset). */
- base?: Partial
- /** Owner's effect timing, surfaced to configuration UIs; defaults to `live`. */
- applies?: SettingsApplies
- /**
- * Reject a resolved section the owner could not act on, for constraints its
- * schema cannot express — a cross-field requirement, or one field's validity
- * depending on another's. Throwing here refuses the *write* that produced the
- * value, so a caller learns at `update`/`replace`/`mutate` instead of storing
- * something that would silently disable the owner.
- *
- * Kept separate from the schema because the schema is also what a
- * configuration surface renders and what an absent section resolves through;
- * folding a cross-field check into it would change both.
- *
- * Once the owner is registered, a stored section that fails this keeps the
- * namespace's last good value and warns, exactly as a schema failure does,
- * so an externally edited document cannot strand a running owner. At
- * 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.
- * @param value - the resolved section, schema-valid by construction.
- */
- validate?: (value: T) => void
-}
-```
-
-`validate` runs after the schema admits a value, so it sees defaults and the composition base exactly as the owner will. `dsh-llm-pi-ai` uses it to refuse a provider profile it could not serve at the write that produced it, rather than storing one that would disable every route in its namespace.
-
-`applies` is a UI hint, not a mechanism: a `restart` owner never watches, so its value is read once at construction and configuration surfaces can badge the pending change.
-
-```ts type-equiv
-/** When a namespace's changes take effect for its owner. */
-type SettingsApplies = 'live' | 'restart'
-```
-
-## Owner scope
-
-The scope is the owner-facing handle. `update` merges a sparse patch over the user section only (never into `base`); `replace` sets the section wholesale, which is the removal/reset path — keys absent from the replacement re-inherit `base` and schema defaults. Writes to one namespace are serialized in call order, and resolved values are deep-frozen snapshots.
-
-```ts type-equiv
-/** Owner-facing handle for one registered namespace. */
-interface SettingsScope {
- /** Current resolved value: schema defaults, then `base`, then the user layer. */
- get(): T
- /**
- * Observe committed changes to this namespace's resolved value. Invocations
- * of one callback run asynchronously, one at a time, in commit order; a
- * rejection is contained and logged like a sync throw. After the disposer
- * returns, no further invocation starts — one already queued is skipped;
- * one already started still settles, and service disposal waits for it.
- * @param callback - invoked after each commit with the next and previous values.
- * @returns the disposer removing this observer.
- */
- watch(callback: (next: T, prev: T) => void | Promise): () => void
- /**
- * 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
- /**
- * 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}.
- */
- replace(section: object): Promise
-}
-```
-
-## Descriptors
-
-`describe()` serializes every registered namespace for configuration surfaces: the schemastery `toJSON()` envelope drives schema-rendered forms, the resolved value fills them, and the detached `base`/`user` layers let a form mark user-overridden fields by presence. `describe({ redactSecrets: true })` — mandatory on every wire surface — strips `role('secret')` fields from all three layers and enumerates their `{path, set}` slots so a page can render write-only inputs without ever receiving a secret.
-
-```ts type-equiv
-/** One registered namespace as surfaced to configuration UIs. */
-interface SettingsDescriptor {
- /** The registered namespace. */
- ns: SettingsNamespace
- /** Serialized schemastery schema (`schema.toJSON()`). */
- schema: unknown
- /** Current resolved value. */
- value: unknown
- /**
- * Monotonic revision of the raw user section this descriptor was read at.
- * Send it back as `expectedRevision` on a write to refuse a stale one.
- */
- revision: number
- /** Registrant's composition `base` layer (detached), when one was declared. */
- base?: unknown
- /**
- * 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.
- */
- user?: unknown
- /** Owner's declared effect timing. */
- applies: SettingsApplies
- /** Schema-declared secret positions; present only under `redactSecrets`. */
- secrets?: RedactedSecret[]
-}
-```
-
-A caller that holds only the redacted descriptor cannot safely rebuild a section, so removals travel as path ops instead. Each descriptor also carries a `revision` over the raw section; a write may send it back as `expectedRevision`, and one that no longer matches is refused rather than applied over the writer that landed first.
-```ts type-equiv
-/**
- * One path-addressed edit to a namespace's user section. Path mutation exists
- * for a caller holding an INCOMPLETE view of the section — a configuration UI
- * reads the redacted descriptor, which by construction never received the
- * `role('secret')` fields. Such a caller can name the field it means without
- * restating the section: a wholesale `replace` rebuilt from a redacted
- * document silently deletes every secret the wire never returned.
- */
-type SettingsPathOp =
- | { op: 'set'; path: readonly string[]; value: unknown }
- | { op: 'unset'; path: readonly string[] }
-```
-
-```ts type-equiv
-/** Options for {@link SettingsProvider.describe}. */
-interface SettingsDescribeOptions {
- /**
- * Strip `role('secret')` fields from `value`/`base`/`user` and enumerate
- * them in each descriptor's `secrets`. Every wire surface MUST pass this;
- * the verbatim default exists for same-process configuration UIs only.
- */
- redactSecrets?: boolean
-}
-```
-
-## Change commits
-
-Every committed change — an in-process write or an externally observed provider edit — emits `settings/updated (ns, next, prev, source)` after the new value is authoritative, and never when the resolved value is deep-equal. The source tag separates the two entry paths.
-
-```ts type-equiv
-/** Origin of one committed settings change. */
-type SettingsUpdateSource = 'update' | 'provider'
-```
-
-## Native document operations
-
-`SettingsDocumentOpenValue` confirms that `settings/openSettingsDocument` prepared the provider-owned document and handed it to the native text editor. This operation does not accept a browser-selected Host path.
+`settings/document-updated` invalidates form descriptors after Loader configuration changes. It is a UI notification; consumers use `loader/volatile-update` only when they need to refresh registration facts.
@@ -173,105 +22,52 @@ type SettingsUpdateSource = 'update' | 'provider'
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
-
+
-### `ctx.settings` — `SettingsProvider` (abstract seam)
+### `ctx.settings` — `SettingsForms`
-Abstract settings service. Providers implement raw-document storage (`load`/`persist`) and push external changes through Settings.publish; the base class owns namespace registration, resolution, validation, change detection, and the `settings/updated` commit event.
+Project Config schemas into forms and own optional instance-level UI policy.
```ts cordis-catalog
-/**
- * Prepare the provider's user-editable document for a native editor. File
- * providers may materialize an absent document before returning its path;
- * non-file providers return undefined.
- * @returns the absolute local document path, or undefined for non-file storage.
+/** Register the calling plugin instance's page policy without changing its Config.
+ * @param presentation Automatic-page policy for this instance; `auto` defaults to true.
+ * @param owner Plugin instance the policy belongs to; defaults to the calling fiber.
+ * @returns Disposer; register it with the calling plugin's effects.
+ * @throws If this instance already has a registered policy.
*/
-prepareDocument(): Promise
+configure(presentation: { auto?: boolean }, owner: Fiber = this.ctx.fiber): () => void
-/**
- * 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 namespace and its observers. An invalid stored section fails the
- * registration itself — the earliest point where the schema can judge it.
- * @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.
+/** Locate the profile patch for native editing.
+ * @returns The existing profile patch path.
*/
-register( ns: Namespace & SettingsNamespaceInput, schema: z, options?: SettingsRegisterOptions, ): SettingsScope
+prepareDocument(): Promise
-/**
- * Attach one optional-settings consumer to this provider. The consumer
- * registers its composition entry as the base layer while this provider is
- * present, then falls back to that entry if the provider detaches.
- * @param owner - consumer context whose unload suppresses fallback work.
- * @param ns - consumer-owned settings namespace.
- * @param schema - schema resolving the namespace.
- * @param entry - composition entry used as the base and fallback value.
- * @param hooks - source sink, change notification, and optional validation.
- * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
- */
-installSection( owner: Context, ns: Namespace & SettingsNamespaceInput, schema: z, entry: T, hooks: SettingsSectionHooks, ): void
-
-/**
- * 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.
+/** Read active plugin schemas and their live values.
+ * @param options Redaction required for remote callers.
+ * @returns Forms keyed by unique profile entry ids.
*/
describe(options?: SettingsDescribeOptions): SettingsDescriptor[]
-/**
- * Read one registered namespace's resolved value.
- * @param ns - the namespace to read.
- * @returns the resolved value, or `undefined` while unregistered.
- * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
+/** Merge editable fields into an entry's config.
+ * @param ns Profile entry id.
+ * @param patch Fields to merge.
+ * @param expectedRevision Revision returned by describe.
*/
-get(ns: Namespace & SettingsNamespaceInput): unknown
+async update(ns: string, patch: object, expectedRevision?: number): Promise
-/**
- * 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
- * namespace that moved past it rejects with {@link SettingsConflictError}.
- * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
+/** Reset all live fields, then set the supplied fields; ordinary config is preserved.
+ * @param ns Profile entry id.
+ * @param section Complete form values.
+ * @param expectedRevision Revision returned by describe.
*/
-async update( ns: Namespace & SettingsNamespaceInput, patch: object, expectedRevision?: number, ): Promise
+async replace(ns: string, section: object, expectedRevision?: number): Promise
-/**
- * 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
- * namespace that moved past it rejects with {@link SettingsConflictError}.
- * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
+/** Apply field edits without restating redacted secrets; unsetting an array index removes its element.
+ * @param ns Profile entry id.
+ * @param ops Ordered form edits.
+ * @param expectedRevision Revision returned by describe.
*/
-async replace( ns: Namespace & SettingsNamespaceInput, section: object, expectedRevision?: number, ): Promise
-
-/**
- * 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
- * namespace that moved past it rejects with {@link SettingsConflictError}.
- * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
- */
-async mutate( ns: Namespace & SettingsNamespaceInput, ops: readonly SettingsPathOp[], expectedRevision?: number, ): Promise
+async mutate(ns: string, ops: readonly SettingsPathOp[], expectedRevision?: number): Promise
```
Source: [`packages/settings/settings/src/index.ts`](../../packages/settings/settings/src/index.ts)
@@ -342,49 +138,18 @@ Source: [`packages/api/settings-controller/src/index.ts`](../../packages/api/set
#### `settings/document-updated` — emit
-One registered namespace's RAW user section changed, whether or not the resolved value did. `settings/updated` is the consumer-facing event and stays deep-equal-gated; this one exists for configuration surfaces, which must learn that a field went from inherited to overridden (same resolved value, different meaning) and that their held revision is stale. Listener containment matches `settings/updated`.
+One profile entry's form values, availability, or page policy changed. Form clients re-read its schema, resolved values, and revision.
```ts cordis-catalog
/**
- * One registered namespace's RAW user section changed, whether or not the
- * resolved value did. `settings/updated` is the consumer-facing event and
- * stays deep-equal-gated; this one exists for configuration surfaces,
- * which must learn that a field went from inherited to overridden (same
- * 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 namespace's new revision.
+ * One profile entry's form values, availability, or page policy changed.
+ * Form clients re-read its schema, resolved values, and revision.
+ * @param ns Profile entry id.
+ * @param revision The entry's new revision.
* @mode emit
*/
'settings/document-updated'(ns: SettingsNamespace, revision: number): void
```
-Source: [`packages/settings/settings/src/types.ts`](../../packages/settings/settings/src/types.ts)
-
-
-
-#### `settings/updated` — emit
-
-Committed change to one registered namespace's resolved value. Emitted after the provider persisted (for `update`) or published (`provider`) the change; never emitted when the resolved value is deep-equal. Listener failures are contained and logged — a sync throw and an async rejection alike — except `INVARIANT`-coded failures, which rethrow after every listener ran; that rethrow reaches the emitter only from synchronous listeners, so invariant checks on this event must not be async functions.
-
-```ts cordis-catalog
-/**
- * Committed change to one registered namespace's resolved value. Emitted
- * after the provider persisted (for `update`) or published (`provider`)
- * the change; never emitted when the resolved value is deep-equal.
- * Listener failures are contained and logged — a sync throw and an async
- * rejection alike — except `INVARIANT`-coded failures, which rethrow
- * after every listener ran; that rethrow reaches the emitter only from
- * synchronous listeners, so invariant checks on this event must not be
- * async functions.
- * @param ns - the namespace whose resolved value changed.
- * @param next - the new resolved value.
- * @param prev - the previous resolved value.
- * @param source - whether the change entered through `update()` or the provider.
- * @mode emit
- */
-'settings/updated'(ns: SettingsNamespace, next: unknown, prev: unknown, source: SettingsUpdateSource): void
-```
-
Source: [`packages/settings/settings/src/types.ts`](../../packages/settings/settings/src/types.ts)
diff --git a/docs/subsystems/settings.zh.md b/docs/subsystems/settings.zh.md
index 88c50e5f99..cb2fbeb694 100644
--- a/docs/subsystems/settings.zh.md
+++ b/docs/subsystems/settings.zh.md
@@ -1,169 +1,18 @@
-# 用户设置
+# 插件配置表单
[English](settings.md) | 中文
-[dsh-settings](../../packages/settings/settings) 的用户设置 seam 持有一份按 namespace 分节的用户文档,并把每个已注册 namespace 解析为:schema 默认值,然后注册方的组合 `base`,最后用户分节。[dsh-settings-file](../../packages/settings/settings-file) 这类提供方存储原始文档并推送外部编辑;消费方插件注册 schema 后读取或观察解析值。组合配置仍留在 `cordis.yml`——namespace 只承载用户可编辑子集。
+[设置服务](../../packages/settings/settings/README.zh.md) 从活动 profile 条目投影 volatile Config 字段。[配置编辑器](../../packages/boot/config-editor/README.zh.md) 通过 Cordis patch 持久化编辑。业务消费者对自己的 Config 引用调用 `.get()`。
-来源:[`packages/settings/settings/src/index.ts`](../../packages/settings/settings/src/index.ts)
+## 标识与值
-## 标识
+表单命名空间是当前 profile 中可唯一定位条目的本地 id。多个插件实例在条目 id 不同时拥有独立表单。普通字段被排除。描述符包含实际值、继承值、显式 profile 覆盖值和乐观修订号。
-namespace 命名用户文档中一个归插件所有的分节。brand 防止调用方将设置 namespace 与在包或进程之间传递的其他 id 混用;构造时校验小写 kebab-case 语法。
+## 编辑
-```ts type-equiv
-/** Nominal id of one registered settings namespace. */
-type SettingsNamespace = Branded<'SettingsNamespace'>
-```
+`update` 合并提交的字段。`replace` 先将即时字段重置为继承配置,再应用提交的字段。`mutate` 操作独立路径,保留客户端响应中未包含的秘密值。每次写入都会验证完整 Config,并在持久化前拒绝过期修订号。
-## 注册
-
-注册把 schemastery schema 绑定到调用方插件 fiber 上的 namespace——dispose(资源释放)该 fiber 即移除 namespace 及其观察者。options 携带组合层、owner 的生效时机,以及一个可选的、用于校验 schema 表达不了的约束的钩子。
-
-```ts type-equiv
-/** Registration options beyond the namespace schema. */
-interface SettingsRegisterOptions {
- /** Composition-layer values resolved below the user layer (entry-config subset). */
- base?: Partial
- /** Owner's effect timing, surfaced to configuration UIs; defaults to `live`. */
- applies?: SettingsApplies
- /**
- * Reject a resolved section the owner could not act on, for constraints its
- * schema cannot express — a cross-field requirement, or one field's validity
- * depending on another's. Throwing here refuses the *write* that produced the
- * value, so a caller learns at `update`/`replace`/`mutate` instead of storing
- * something that would silently disable the owner.
- *
- * Kept separate from the schema because the schema is also what a
- * configuration surface renders and what an absent section resolves through;
- * folding a cross-field check into it would change both.
- *
- * Once the owner is registered, a stored section that fails this keeps the
- * namespace's last good value and warns, exactly as a schema failure does,
- * so an externally edited document cannot strand a running owner. At
- * 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.
- * @param value - the resolved section, schema-valid by construction.
- */
- validate?: (value: T) => void
-}
-```
-
-`validate` 在 schema 接纳该值之后运行,因此它看到的默认值和组合 base 与 owner 实际看到的完全一致。`dsh-llm-pi-ai` 用它在写入处拒绝自己无法服务的提供方 profile,而不是先存下来、再让该 namespace 下每条路由失效。
-
-`applies` 是 UI 提示而非机制:`restart` 的 owner 从不 watch,其值在构造期读取一次,配置界面可为待生效变更加标。
-
-```ts type-equiv
-/** When a namespace's changes take effect for its owner. */
-type SettingsApplies = 'live' | 'restart'
-```
-
-## Owner scope
-
-scope 是面向 owner 的句柄。`update` 把稀疏 patch 只合并进用户分节(绝不进 `base`);`replace` 整体替换分节,是删除/重置路径——替换中缺席的键重新继承 `base` 与 schema 默认值。同一 namespace 的写入按调用顺序串行,解析值是深冻结快照。
-
-```ts type-equiv
-/** Owner-facing handle for one registered namespace. */
-interface SettingsScope {
- /** Current resolved value: schema defaults, then `base`, then the user layer. */
- get(): T
- /**
- * Observe committed changes to this namespace's resolved value. Invocations
- * of one callback run asynchronously, one at a time, in commit order; a
- * rejection is contained and logged like a sync throw. After the disposer
- * returns, no further invocation starts — one already queued is skipped;
- * one already started still settles, and service disposal waits for it.
- * @param callback - invoked after each commit with the next and previous values.
- * @returns the disposer removing this observer.
- */
- watch(callback: (next: T, prev: T) => void | Promise): () => void
- /**
- * 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
- /**
- * 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}.
- */
- replace(section: object): Promise
-}
-```
-
-## 描述符
-
-`describe()` 为配置界面序列化每个已注册 namespace:schemastery 的 `toJSON()` 封装结构驱动 schema 渲染的表单,解析值填充表单,分离出的 `base`/`user` 层让表单按字段是否出现在 user 层标注「用户已覆盖」。`describe({ redactSecrets: true })`——每个对外传输接口都必须传入——从三层剥离 `role('secret')` 字段并枚举其 `{path, set}` slot,页面因此能渲染只写输入框而永远收不到机密值。
-
-```ts type-equiv
-/** One registered namespace as surfaced to configuration UIs. */
-interface SettingsDescriptor {
- /** The registered namespace. */
- ns: SettingsNamespace
- /** Serialized schemastery schema (`schema.toJSON()`). */
- schema: unknown
- /** Current resolved value. */
- value: unknown
- /**
- * Monotonic revision of the raw user section this descriptor was read at.
- * Send it back as `expectedRevision` on a write to refuse a stale one.
- */
- revision: number
- /** Registrant's composition `base` layer (detached), when one was declared. */
- base?: unknown
- /**
- * 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.
- */
- user?: unknown
- /** Owner's declared effect timing. */
- applies: SettingsApplies
- /** Schema-declared secret positions; present only under `redactSecrets`. */
- secrets?: RedactedSecret[]
-}
-```
-
-只持有脱敏 descriptor 的调用方无法安全地重建分节,因此删除改以路径 op 传递。每个 descriptor 还携带针对原始分节的 `revision`;写入可以把它作为 `expectedRevision` 送回,不再匹配的写入会被拒绝,而不会覆盖先落地的写入。
-```ts type-equiv
-/**
- * One path-addressed edit to a namespace's user section. Path mutation exists
- * for a caller holding an INCOMPLETE view of the section — a configuration UI
- * reads the redacted descriptor, which by construction never received the
- * `role('secret')` fields. Such a caller can name the field it means without
- * restating the section: a wholesale `replace` rebuilt from a redacted
- * document silently deletes every secret the wire never returned.
- */
-type SettingsPathOp =
- | { op: 'set'; path: readonly string[]; value: unknown }
- | { op: 'unset'; path: readonly string[] }
-```
-
-```ts type-equiv
-/** Options for {@link SettingsProvider.describe}. */
-interface SettingsDescribeOptions {
- /**
- * Strip `role('secret')` fields from `value`/`base`/`user` and enumerate
- * them in each descriptor's `secrets`. Every wire surface MUST pass this;
- * the verbatim default exists for same-process configuration UIs only.
- */
- redactSecrets?: boolean
-}
-```
-
-## 变更提交
-
-每次提交的变更——进程内写入或提供方观察到的外部编辑——在新值成为权威值之后发出 `settings/updated (ns, next, prev, source)`,解析值深相等时绝不发出。source 标记区分两条入口路径。
-
-```ts type-equiv
-/** Origin of one committed settings change. */
-type SettingsUpdateSource = 'update' | 'provider'
-```
-
-## 原生文档操作
-
-`SettingsDocumentOpenValue` 确认 `settings/openSettingsDocument` 已准备好 provider 持有的文档,并将其交给原生文本编辑器。此操作不接受由浏览器选择的 Host 路径。
+`settings/document-updated` 在 Loader 配置变化后使表单描述符失效。这是 UI 通知;消费者仅在需要刷新注册信息时使用 `loader/volatile-update`。
@@ -173,105 +22,52 @@ type SettingsUpdateSource = 'update' | 'provider'
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.zh.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
-
+
-### `ctx.settings` — `SettingsProvider` (abstract seam)
+### `ctx.settings` — `SettingsForms`
-Abstract settings service. Providers implement raw-document storage (`load`/`persist`) and push external changes through Settings.publish; the base class owns namespace registration, resolution, validation, change detection, and the `settings/updated` commit event.
+Project Config schemas into forms and own optional instance-level UI policy.
```ts cordis-catalog
-/**
- * Prepare the provider's user-editable document for a native editor. File
- * providers may materialize an absent document before returning its path;
- * non-file providers return undefined.
- * @returns the absolute local document path, or undefined for non-file storage.
+/** Register the calling plugin instance's page policy without changing its Config.
+ * @param presentation Automatic-page policy for this instance; `auto` defaults to true.
+ * @param owner Plugin instance the policy belongs to; defaults to the calling fiber.
+ * @returns Disposer; register it with the calling plugin's effects.
+ * @throws If this instance already has a registered policy.
*/
-prepareDocument(): Promise
+configure(presentation: { auto?: boolean }, owner: Fiber = this.ctx.fiber): () => void
-/**
- * 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 namespace and its observers. An invalid stored section fails the
- * registration itself — the earliest point where the schema can judge it.
- * @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.
+/** Locate the profile patch for native editing.
+ * @returns The existing profile patch path.
*/
-register( ns: Namespace & SettingsNamespaceInput, schema: z, options?: SettingsRegisterOptions, ): SettingsScope
+prepareDocument(): Promise
-/**
- * Attach one optional-settings consumer to this provider. The consumer
- * registers its composition entry as the base layer while this provider is
- * present, then falls back to that entry if the provider detaches.
- * @param owner - consumer context whose unload suppresses fallback work.
- * @param ns - consumer-owned settings namespace.
- * @param schema - schema resolving the namespace.
- * @param entry - composition entry used as the base and fallback value.
- * @param hooks - source sink, change notification, and optional validation.
- * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
- */
-installSection( owner: Context, ns: Namespace & SettingsNamespaceInput, schema: z, entry: T, hooks: SettingsSectionHooks, ): void
-
-/**
- * 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.
+/** Read active plugin schemas and their live values.
+ * @param options Redaction required for remote callers.
+ * @returns Forms keyed by unique profile entry ids.
*/
describe(options?: SettingsDescribeOptions): SettingsDescriptor[]
-/**
- * Read one registered namespace's resolved value.
- * @param ns - the namespace to read.
- * @returns the resolved value, or `undefined` while unregistered.
- * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
+/** Merge editable fields into an entry's config.
+ * @param ns Profile entry id.
+ * @param patch Fields to merge.
+ * @param expectedRevision Revision returned by describe.
*/
-get(ns: Namespace & SettingsNamespaceInput): unknown
+async update(ns: string, patch: object, expectedRevision?: number): Promise
-/**
- * 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
- * namespace that moved past it rejects with {@link SettingsConflictError}.
- * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
+/** Reset all live fields, then set the supplied fields; ordinary config is preserved.
+ * @param ns Profile entry id.
+ * @param section Complete form values.
+ * @param expectedRevision Revision returned by describe.
*/
-async update( ns: Namespace & SettingsNamespaceInput, patch: object, expectedRevision?: number, ): Promise
+async replace(ns: string, section: object, expectedRevision?: number): Promise
-/**
- * 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
- * namespace that moved past it rejects with {@link SettingsConflictError}.
- * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
+/** Apply field edits without restating redacted secrets; unsetting an array index removes its element.
+ * @param ns Profile entry id.
+ * @param ops Ordered form edits.
+ * @param expectedRevision Revision returned by describe.
*/
-async replace( ns: Namespace & SettingsNamespaceInput, section: object, expectedRevision?: number, ): Promise
-
-/**
- * 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
- * namespace that moved past it rejects with {@link SettingsConflictError}.
- * @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
- */
-async mutate( ns: Namespace & SettingsNamespaceInput, ops: readonly SettingsPathOp[], expectedRevision?: number, ): Promise
+async mutate(ns: string, ops: readonly SettingsPathOp[], expectedRevision?: number): Promise
```
Source: [`packages/settings/settings/src/index.ts`](../../packages/settings/settings/src/index.ts)
@@ -342,49 +138,18 @@ Source: [`packages/api/settings-controller/src/index.ts`](../../packages/api/set
#### `settings/document-updated` — emit
-One registered namespace's RAW user section changed, whether or not the resolved value did. `settings/updated` is the consumer-facing event and stays deep-equal-gated; this one exists for configuration surfaces, which must learn that a field went from inherited to overridden (same resolved value, different meaning) and that their held revision is stale. Listener containment matches `settings/updated`.
+One profile entry's form values, availability, or page policy changed. Form clients re-read its schema, resolved values, and revision.
```ts cordis-catalog
/**
- * One registered namespace's RAW user section changed, whether or not the
- * resolved value did. `settings/updated` is the consumer-facing event and
- * stays deep-equal-gated; this one exists for configuration surfaces,
- * which must learn that a field went from inherited to overridden (same
- * 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 namespace's new revision.
+ * One profile entry's form values, availability, or page policy changed.
+ * Form clients re-read its schema, resolved values, and revision.
+ * @param ns Profile entry id.
+ * @param revision The entry's new revision.
* @mode emit
*/
'settings/document-updated'(ns: SettingsNamespace, revision: number): void
```
-Source: [`packages/settings/settings/src/types.ts`](../../packages/settings/settings/src/types.ts)
-
-
-
-#### `settings/updated` — emit
-
-Committed change to one registered namespace's resolved value. Emitted after the provider persisted (for `update`) or published (`provider`) the change; never emitted when the resolved value is deep-equal. Listener failures are contained and logged — a sync throw and an async rejection alike — except `INVARIANT`-coded failures, which rethrow after every listener ran; that rethrow reaches the emitter only from synchronous listeners, so invariant checks on this event must not be async functions.
-
-```ts cordis-catalog
-/**
- * Committed change to one registered namespace's resolved value. Emitted
- * after the provider persisted (for `update`) or published (`provider`)
- * the change; never emitted when the resolved value is deep-equal.
- * Listener failures are contained and logged — a sync throw and an async
- * rejection alike — except `INVARIANT`-coded failures, which rethrow
- * after every listener ran; that rethrow reaches the emitter only from
- * synchronous listeners, so invariant checks on this event must not be
- * async functions.
- * @param ns - the namespace whose resolved value changed.
- * @param next - the new resolved value.
- * @param prev - the previous resolved value.
- * @param source - whether the change entered through `update()` or the provider.
- * @mode emit
- */
-'settings/updated'(ns: SettingsNamespace, next: unknown, prev: unknown, source: SettingsUpdateSource): void
-```
-
Source: [`packages/settings/settings/src/types.ts`](../../packages/settings/settings/src/types.ts)
diff --git a/docs/subsystems/voice-input.i18n.yaml b/docs/subsystems/voice-input.i18n.yaml
index a7ef725b7c..b0886646aa 100644
--- a/docs/subsystems/voice-input.i18n.yaml
+++ b/docs/subsystems/voice-input.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/voice-input.md
-voice-input.md: f1d3f36d067f756235e976fa6eafe70f94b19ccc
-voice-input.zh.md: a32b539fc1237bb70d68668de939084d5f634a82
+voice-input.md: 32517d418512078cde5af221594d94171054abf4
+voice-input.zh.md: 162a28162d8a171ac214dd59a2096752172c8081
diff --git a/docs/subsystems/voice-input.md b/docs/subsystems/voice-input.md
index f1d3f36d06..32517d4185 100644
--- a/docs/subsystems/voice-input.md
+++ b/docs/subsystems/voice-input.md
@@ -119,9 +119,9 @@ async *follow(caller: AbortSignal): AsyncIterable
snapshot(): SpeechSnapshot
/**
- * Persist changed preference fields; the resulting language must be accepted by the selected provider.
+ * Persist changed selection fields into this plugin's profile entry; the resulting language must be accepted by the selected provider.
* @param patch - explicit provider or language changes.
- * @returns after persistence and the resolved preference update.
+ * @returns after the profile write and the live update it applies.
*/
async configure(patch: SpeechSelectionPatch): Promise
diff --git a/docs/subsystems/voice-input.zh.md b/docs/subsystems/voice-input.zh.md
index a32b539fc1..162a28162d 100644
--- a/docs/subsystems/voice-input.zh.md
+++ b/docs/subsystems/voice-input.zh.md
@@ -119,9 +119,9 @@ async *follow(caller: AbortSignal): AsyncIterable
snapshot(): SpeechSnapshot
/**
- * Persist changed preference fields; the resulting language must be accepted by the selected provider.
+ * Persist changed selection fields into this plugin's profile entry; the resulting language must be accepted by the selected provider.
* @param patch - explicit provider or language changes.
- * @returns after persistence and the resolved preference update.
+ * @returns after the profile write and the live update it applies.
*/
async configure(patch: SpeechSelectionPatch): Promise
diff --git a/docs/user/guide/providers.i18n.yaml b/docs/user/guide/providers.i18n.yaml
index f0043c374d..e3c0fdb127 100644
--- a/docs/user/guide/providers.i18n.yaml
+++ b/docs/user/guide/providers.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/user/guide/providers.md
-providers.md: ef016ef39d5e34f3ce3dd2feb6213b9597668592
-providers.zh.md: 15fd229ce222a020e897f08c15afcd843791a3bb
+providers.md: 81ccba8b0665fa1f7666b5724aab4a8b3d183bbf
+providers.zh.md: 1afb7e7bf616206290e5cc4a90ef45b4250a9596
diff --git a/docs/user/guide/providers.md b/docs/user/guide/providers.md
index ef016ef39d..81ccba8b06 100644
--- a/docs/user/guide/providers.md
+++ b/docs/user/guide/providers.md
@@ -40,61 +40,66 @@ If a saved default names a provider that was deleted, the composer displays **Se
## Advanced configuration
-The generated [plugin configuration catalog](../../config-catalog.md) lists every supported field and default for every plugin; [`dsh-llm-pi-ai`](../../config-catalog.md#deepseek-aidsh-llm-pi-ai) is the provider section this page configures. The [`dsh-llm-pi-ai`](../../../packages/llm/llm-pi-ai/README.md) and [`dsh-llm-deepseek`](../../../packages/llm/llm-deepseek/README.md) references own direct `settings.yaml` configuration, catalog resolution, reasoning controls, credentials, and adapter errors.
+The generated [plugin configuration catalog](../../config-catalog.md) lists every supported field and default for every plugin; [`dsh-llm-pi-ai`](../../config-catalog.md#deepseek-aidsh-llm-pi-ai) is the provider section this page configures. The [`dsh-llm-pi-ai`](../../../packages/llm/llm-pi-ai/README.md) and [`dsh-llm-deepseek`](../../../packages/llm/llm-deepseek/README.md) references own direct `cordis.patch.yml` configuration, catalog resolution, reasoning controls, credentials, and adapter errors.
::: tip Additional settings
-The Models page exposes the API key, display name, base URL, API protocol, and each model's id, display name, context window, max output tokens, and input types. Configure reasoning effort levels, request-compatibility switches, headers, timeouts, and retry policy in `$DSH_HOME/settings.yaml`, the same document the page writes. Edit it directly, or, when the browser runs on the same machine as the server, open it with **Open configuration file** in the Settings header; the adapters re-read it on the next request, so nothing needs a restart. The subsections below cover the fields most gateways need.
+The Models page exposes the API key, display name, base URL, API protocol, and each model's id, display name, context window, max output tokens, and input types. Configure reasoning effort levels, request-compatibility switches, headers, timeouts, and retry policy in `$DSH_HOME/profiles//cordis.patch.yml`, the same document the page writes. Edit it directly, or, when the browser runs on the same machine as the server, open it with **Open configuration file** in the Settings header; the adapters re-read it on the next request, so nothing needs a restart. The subsections below cover the fields most gateways need.
:::
### Image input
In **Settings → Models**, edit the provider, open **Customized settings**, and expand the model's **Model options**. **Input types** occupies its own row below the capacity fields. Select **Image** for a model that accepts images, and save. **Text** starts selected for a new custom model with no inherited image capability. At least one type must remain selected; select Image before clearing Text for an image-only model.
-The checkboxes save `input` for pi-ai models and `inputModalities` for the direct DeepSeek adapter. You can also edit the model in `$DSH_HOME/settings.yaml`; for example, this custom pi-ai provider declares one text-only model and one vision model:
+The checkboxes save `input` for pi-ai models and `inputModalities` for the direct DeepSeek adapter. You can also edit the model in `$DSH_HOME/profiles//cordis.patch.yml`; for example, this custom pi-ai provider declares one text-only model and one vision model:
+
+These examples show config fields inside a profile patch. A Cordis config override replaces the complete entry config; preserve other providers and fields when editing an existing override.
```yaml
-llm-pi-ai:
- providers:
- my-gateway:
- apiKeyEnv: GATEWAY_API_KEY
- api: openai-completions
- baseURL: https://gateway.example/v1
- models:
- - id: legacy-chat
- - id: vision-preview
- input: [text, image]
+- id: llm-pi-ai
+ config:
+ providers:
+ my-gateway:
+ apiKeyEnv: GATEWAY_API_KEY
+ api: openai-completions
+ baseURL: https://gateway.example/v1
+ models:
+ - id: legacy-chat
+ - id: vision-preview
+ input: [text, image]
```
Pi-ai's `input` accepts `text` and `image` and applies to that model alone. An explicit nonempty selection takes priority. An omitted or empty `input` inherits the installed catalog's input types, then the route's `defaultInput`, which defaults to `[text]`. The checkboxes display these inherited values without saving an override when you merely open the row.
DeepSeek treats an omitted `inputModalities` as text-only and rejects an empty list. Clearing Image also removes that model's `imagePixelBudget` and `imageMaxBytes`, because DeepSeek rejects image limits on a text-only model. Set those limits again if you later enable images and need custom limits.
-To restore inheritance after editing the checkboxes, remove the model's `input` or `inputModalities` field from `settings.yaml`. **Restore defaults** removes the entire model-catalog override, including other model edits, so use it only when you want to restore the whole catalog.
+To restore inheritance after editing the checkboxes, remove the model's `input` or `inputModalities` field from `cordis.patch.yml`. **Restore defaults** removes the entire model-catalog override, including other model edits, so use it only when you want to restore the whole catalog.
If every model you entered by hand takes images, set the fallback once on the route instead of on each of them:
```yaml
-llm-pi-ai:
- providers:
- vision-gateway:
- apiKeyEnv: GATEWAY_API_KEY
- api: openai-completions
- baseURL: https://vision.example/v1
- defaultInput: [text, image]
- models:
- - id: first-model
- - id: second-model
+- id: llm-pi-ai
+ config:
+ providers:
+ vision-gateway:
+ apiKeyEnv: GATEWAY_API_KEY
+ api: openai-completions
+ baseURL: https://vision.example/v1
+ defaultInput: [text, image]
+ models:
+ - id: first-model
+ - id: second-model
```
`defaultInput` is a fallback, not an override, and defaults to `[text]`: on a built-in provider it answers only for models its catalog does not describe, so it never removes images from a catalog model that has them. Narrow one of those with that model's own `input`. When a built-in provider has no explicit `models` list, write it under `modelOverrides`, keyed by model id:
```yaml
-llm-pi-ai:
- providers:
- anthropic:
- modelOverrides:
- claude-sonnet-4-5:
- input: [text]
+- id: llm-pi-ai
+ config:
+ providers:
+ anthropic:
+ modelOverrides:
+ claude-sonnet-4-5:
+ input: [text]
```
In pi-ai configuration, every list must name at least one modality except a model's own `input`, where an empty list means the same as omitting it. An unknown modality is refused wherever it is written.
@@ -103,22 +108,23 @@ Both fields state a claim about your endpoint rather than checking it. A model t
### Reasoning effort
-The model picker offers an **Effort** menu for a model that declares reasoning levels. A built-in provider's models inherit their levels from the installed catalog. A model you enter by hand declares none, so the Effort entry does not appear in the menu and the endpoint's own default decides whether the model thinks. Declare the levels with `reasoningEfforts` in `$DSH_HOME/settings.yaml`:
+The model picker offers an **Effort** menu for a model that declares reasoning levels. A built-in provider's models inherit their levels from the installed catalog. A model you enter by hand declares none, so the Effort entry does not appear in the menu and the endpoint's own default decides whether the model thinks. Declare the levels with `reasoningEfforts` in `$DSH_HOME/profiles//cordis.patch.yml`:
```yaml
-llm-pi-ai:
- providers:
- my-gateway:
- apiKeyEnv: GATEWAY_API_KEY
- api: openai-completions
- baseURL: https://gateway.example/v1
- reasoning: high
- models:
- - id: my-reasoner
- reasoningEfforts:
- off:
- high: high
- max: max
+- id: llm-pi-ai
+ config:
+ providers:
+ my-gateway:
+ apiKeyEnv: GATEWAY_API_KEY
+ api: openai-completions
+ baseURL: https://gateway.example/v1
+ reasoning: high
+ models:
+ - id: my-reasoner
+ reasoningEfforts:
+ off:
+ high: high
+ max: max
```
Each key is a level the menu offers, and its value is the spelling sent on the wire as `reasoning_effort`, so `max: xhigh` renames a level for a gateway with its own vocabulary. Only `off` may stay empty, because for most endpoints not thinking is the parameter's absence. The route's `reasoning` is the level used while a session has picked none; choosing an effort in the picker saves it, with the model, as the default for new sessions.
@@ -139,28 +145,30 @@ An `off` left empty sends nothing, which only stops a model that thinks on reque
A built-in provider's model whose gateway does not reason loses its levels with `reasoningEfforts: false` under `modelOverrides`; selecting an effort for it is then refused as `UNSUPPORTED_REASONING_EFFORT`. DeepSeek's own route needs none of this: its models already offer `off`, `low`, `high`, and `max`, and `llm-deepseek.reasoningEffort` sets the default the picker starts from:
```yaml
-llm-deepseek:
- reasoningEffort: max
+- id: llm-deepseek
+ config:
+ reasoningEffort: max
```
### Request compatibility
A gateway can hold a working key at a reachable address and still refuse every request. pi-ai decides the shape of a request — which role carries the system prompt, which field caps the output, how a thinking level travels — from the endpoint's URL, and an address it does not recognize is addressed as though it were OpenAI itself. Most OpenAI-compatible gateways refuse at least one thing OpenAI accepts.
-Two account for most of it. A model that declares reasoning has its system prompt sent as `role: "developer"`, which many gateways reject outright, and the output cap is sent as `max_completion_tokens`, which a server that only knows `max_tokens` refuses. The form has no field for either; correct them on the route in `$DSH_HOME/settings.yaml`:
+Two account for most of it. A model that declares reasoning has its system prompt sent as `role: "developer"`, which many gateways reject outright, and the output cap is sent as `max_completion_tokens`, which a server that only knows `max_tokens` refuses. The form has no field for either; correct them on the route in `$DSH_HOME/profiles//cordis.patch.yml`:
```yaml
-llm-pi-ai:
- providers:
- my-gateway:
- apiKeyEnv: GATEWAY_API_KEY
- api: openai-completions
- baseURL: https://gateway.example/v1
- compat:
- supportsDeveloperRole: false
- maxTokensField: max_tokens
- models:
- - id: my-model
+- id: llm-pi-ai
+ config:
+ providers:
+ my-gateway:
+ apiKeyEnv: GATEWAY_API_KEY
+ api: openai-completions
+ baseURL: https://gateway.example/v1
+ compat:
+ supportsDeveloperRole: false
+ maxTokensField: max_tokens
+ models:
+ - id: my-model
```
A route's `compat` is the default for its models, and a model's own wins field by field, so one model can be corrected without restating the route:
@@ -187,7 +195,7 @@ Every switch, its accepted values, and the protocols that take it are listed und
- **Fetching available models reports neither a `data` array nor a `models` object** — The endpoint's listing is in a format discovery does not read. Enter the models by hand.
- **The gateway refuses every request although the key and URL are right** — Its request shape differs from OpenAI's. Start with `compat.supportsDeveloperRole: false` and `compat.maxTokensField: max_tokens` on the route.
- **Only reasoning models fail** — pi-ai sends their system prompt as the `developer` role, which the gateway rejects. Set `compat.supportsDeveloperRole: false`.
-- **The Effort menu does not appear for a model you entered by hand** — It declares no levels. Add `reasoningEfforts` to the model in `settings.yaml`.
+- **The Effort menu does not appear for a model you entered by hand** — It declares no levels. Add `reasoningEfforts` to the model in `cordis.patch.yml`.
- **`off` does not stop a DeepSeek model from thinking** — An empty `off` sends no reasoning field at all, and an endpoint that thinks by default keeps thinking. Set `compat.thinkingFormat: deepseek` on the model or the route.
- **A compat switch is refused as having no value** — A key written with nothing after the colon. Give it a value, or remove the key to keep the installed catalog's.
- **An image is refused before sending** — The model declares no image modality. Give a custom provider's model `input: [text, image]`; on DeepSeek's own route, select an image-capable entry from the configured catalog (`deepseek-flash` by default) and confirm that your gateway serves that model with image input.
diff --git a/docs/user/guide/providers.zh.md b/docs/user/guide/providers.zh.md
index 15fd229ce2..1afb7e7bf6 100644
--- a/docs/user/guide/providers.zh.md
+++ b/docs/user/guide/providers.zh.md
@@ -40,61 +40,66 @@ Provider ID 是永久的,因为请求、已保存会话、模型默认值和
## 进阶配置
-自动生成的[插件配置目录](../../config-catalog.zh.md)列出每个插件的所有受支持字段与默认值;[`dsh-llm-pi-ai`](../../config-catalog.zh.md#deepseek-aidsh-llm-pi-ai) 就是本页所配置的那个提供商段落。[`dsh-llm-pi-ai`](../../../packages/llm/llm-pi-ai/README.zh.md) 和 [`dsh-llm-deepseek`](../../../packages/llm/llm-deepseek/README.zh.md) 参考文档负责直接 `settings.yaml` 配置、目录解析、推理控制、凭据与适配器错误。
+自动生成的[插件配置目录](../../config-catalog.zh.md)列出每个插件的所有受支持字段与默认值;[`dsh-llm-pi-ai`](../../config-catalog.zh.md#deepseek-aidsh-llm-pi-ai) 就是本页所配置的那个提供商段落。[`dsh-llm-pi-ai`](../../../packages/llm/llm-pi-ai/README.zh.md) 和 [`dsh-llm-deepseek`](../../../packages/llm/llm-deepseek/README.zh.md) 参考文档负责直接 `cordis.patch.yml` 配置、目录解析、推理控制、凭据与适配器错误。
::: tip 其他设置
-模型页提供 API 密钥、显示名称、API 地址、API 协议,以及每个模型的 ID、显示名称、上下文窗口、最大输出 token 数和输入类型。推理等级、请求兼容性开关、请求头、超时和重试策略在 `$DSH_HOME/settings.yaml` 中设置,也就是模型页写入的同一份文档。可以直接编辑它;浏览器与服务器在同一台机器时,也可以点击设置页顶部的**打开配置文件**打开它。适配器会在下一次请求时重新读取,无需重启任何东西。下面各小节介绍多数网关会用到的字段。
+模型页提供 API 密钥、显示名称、API 地址、API 协议,以及每个模型的 ID、显示名称、上下文窗口、最大输出 token 数和输入类型。推理等级、请求兼容性开关、请求头、超时和重试策略在 `$DSH_HOME/profiles//cordis.patch.yml` 中设置,也就是模型页写入的同一份文档。可以直接编辑它;浏览器与服务器在同一台机器时,也可以点击设置页顶部的**打开配置文件**打开它。适配器会在下一次请求时重新读取,无需重启任何东西。下面各小节介绍多数网关会用到的字段。
:::
### 图片输入
在**设置 → 模型**中编辑提供商,打开**自定义设置**并展开该模型的**模型选项**。**输入类型**独占容量字段下方的一行。对于支持图片的模型,勾选**图片**并保存。没有继承图片能力的新自定义模型默认勾选**文本**。至少保留一种输入类型;仅图片模型需先勾选图片,再取消文本。
-复选框将 pi-ai 模型的选择保存为 `input`,将直连 DeepSeek 适配器的选择保存为 `inputModalities`。也可以在 `$DSH_HOME/settings.yaml` 中编辑模型;例如,以下自定义 pi-ai 提供商声明了一个纯文本模型和一个视觉模型:
+复选框将 pi-ai 模型的选择保存为 `input`,将直连 DeepSeek 适配器的选择保存为 `inputModalities`。也可以在 `$DSH_HOME/profiles//cordis.patch.yml` 中编辑模型;例如,以下自定义 pi-ai 提供商声明了一个纯文本模型和一个视觉模型:
+
+这些示例展示 profile patch 中的配置字段。Cordis 配置覆盖会替换完整条目配置;编辑已有覆盖项时,请保留其他 provider 和字段。
```yaml
-llm-pi-ai:
- providers:
- my-gateway:
- apiKeyEnv: GATEWAY_API_KEY
- api: openai-completions
- baseURL: https://gateway.example/v1
- models:
- - id: legacy-chat
- - id: vision-preview
- input: [text, image]
+- id: llm-pi-ai
+ config:
+ providers:
+ my-gateway:
+ apiKeyEnv: GATEWAY_API_KEY
+ api: openai-completions
+ baseURL: https://gateway.example/v1
+ models:
+ - id: legacy-chat
+ - id: vision-preview
+ input: [text, image]
```
Pi-ai 的 `input` 接受 `text` 和 `image`,且只作用于该模型。显式的非空选择优先。省略或为空的 `input` 先继承已安装目录的输入类型,再回退到路由的 `defaultInput`,后者默认为 `[text]`。复选框会显示这些继承值,仅打开模型行不会保存覆盖值。
DeepSeek 将省略的 `inputModalities` 视为纯文本,并拒绝空列表。取消图片还会移除该模型的 `imagePixelBudget` 和 `imageMaxBytes`,因为 DeepSeek 拒绝纯文本模型上的图片限制。以后重新启用图片且需要自定义限制时,需再次设置这些限制。
-修改复选框后如需恢复继承,可在 `settings.yaml` 中移除模型的 `input` 或 `inputModalities` 字段。**恢复默认模型**会移除整个模型目录覆盖,包括其他模型编辑,因此仅在需要恢复整个目录时使用。
+修改复选框后如需恢复继承,可在 `cordis.patch.yml` 中移除模型的 `input` 或 `inputModalities` 字段。**恢复默认模型**会移除整个模型目录覆盖,包括其他模型编辑,因此仅在需要恢复整个目录时使用。
如果你手动录入的模型全都接受图片,可以在路由上设置一次回退值,不必逐个模型写:
```yaml
-llm-pi-ai:
- providers:
- vision-gateway:
- apiKeyEnv: GATEWAY_API_KEY
- api: openai-completions
- baseURL: https://vision.example/v1
- defaultInput: [text, image]
- models:
- - id: first-model
- - id: second-model
+- id: llm-pi-ai
+ config:
+ providers:
+ vision-gateway:
+ apiKeyEnv: GATEWAY_API_KEY
+ api: openai-completions
+ baseURL: https://vision.example/v1
+ defaultInput: [text, image]
+ models:
+ - id: first-model
+ - id: second-model
```
`defaultInput` 是回退值而不是覆盖值,默认为 `[text]`:在内置提供商上,它只为其目录未描述的模型作答,因此绝不会把目录中本就具备图片能力的模型的该能力去掉。要收窄这类模型,请用它自己的 `input`。内置提供商没有显式 `models` 列表时,写在 `modelOverrides` 下,以模型 id 为键:
```yaml
-llm-pi-ai:
- providers:
- anthropic:
- modelOverrides:
- claude-sonnet-4-5:
- input: [text]
+- id: llm-pi-ai
+ config:
+ providers:
+ anthropic:
+ modelOverrides:
+ claude-sonnet-4-5:
+ input: [text]
```
在 pi-ai 配置中,除模型自身的 `input` 外,每个列表都至少要写一项模态;模型自身的空列表与省略它同义。未知模态在任何位置写入都会被拒绝。
@@ -103,22 +108,23 @@ llm-pi-ai:
### 推理等级
-对于声明了推理等级的模型,模型选择器会提供**推理等级**菜单。内置提供商的模型从已安装目录继承其等级。手动录入的模型不声明任何等级,因此模型菜单里不会出现推理等级项,由端点自身的默认值决定模型是否思考。请在 `$DSH_HOME/settings.yaml` 中用 `reasoningEfforts` 声明等级:
+对于声明了推理等级的模型,模型选择器会提供**推理等级**菜单。内置提供商的模型从已安装目录继承其等级。手动录入的模型不声明任何等级,因此模型菜单里不会出现推理等级项,由端点自身的默认值决定模型是否思考。请在 `$DSH_HOME/profiles//cordis.patch.yml` 中用 `reasoningEfforts` 声明等级:
```yaml
-llm-pi-ai:
- providers:
- my-gateway:
- apiKeyEnv: GATEWAY_API_KEY
- api: openai-completions
- baseURL: https://gateway.example/v1
- reasoning: high
- models:
- - id: my-reasoner
- reasoningEfforts:
- off:
- high: high
- max: max
+- id: llm-pi-ai
+ config:
+ providers:
+ my-gateway:
+ apiKeyEnv: GATEWAY_API_KEY
+ api: openai-completions
+ baseURL: https://gateway.example/v1
+ reasoning: high
+ models:
+ - id: my-reasoner
+ reasoningEfforts:
+ off:
+ high: high
+ max: max
```
每个键都是菜单提供的一个等级,其值是在协议上以 `reasoning_effort` 发送的写法,因此 `max: xhigh` 可以为自有一套词汇的网关重命名某个等级。只有 `off` 可以留空,因为对多数端点来说,不思考就是不传该参数。路由的 `reasoning` 是会话尚未选择等级时采用的等级;在选择器中选定某个等级后,它会与模型一起保存为新会话的默认值。
@@ -139,28 +145,30 @@ llm-pi-ai:
网关并不提供推理能力的内置提供商模型,可在 `modelOverrides` 下用 `reasoningEfforts: false` 去掉其等级;之后再为它选择等级会被拒绝并报 `UNSUPPORTED_REASONING_EFFORT`。DeepSeek 自身的路由不需要以上任何配置:其模型已经提供 `off`、`low`、`high` 和 `max`,`llm-deepseek.reasoningEffort` 设置选择器的起始默认值:
```yaml
-llm-deepseek:
- reasoningEffort: max
+- id: llm-deepseek
+ config:
+ reasoningEffort: max
```
### 请求兼容性
网关可能持有可用的密钥、地址也通得到,却仍然拒绝每一个请求。pi-ai 依据端点的 URL 决定请求的形状——系统提示词由哪个角色承载、输出上限写在哪个字段、思考级别如何传输——而对于它无法识别的地址,会当作 OpenAI 本身来对待。多数 OpenAI 兼容网关至少会拒绝 OpenAI 所接受的某一样东西。
-其中两样占了绝大多数。声明了推理能力的模型,其系统提示词会以 `role: "developer"` 发出,很多网关直接拒绝;输出上限则写作 `max_completion_tokens`,只认 `max_tokens` 的服务端会拒绝。表单里没有这两个字段;请在 `$DSH_HOME/settings.yaml` 的路由上更正:
+其中两样占了绝大多数。声明了推理能力的模型,其系统提示词会以 `role: "developer"` 发出,很多网关直接拒绝;输出上限则写作 `max_completion_tokens`,只认 `max_tokens` 的服务端会拒绝。表单里没有这两个字段;请在 `$DSH_HOME/profiles//cordis.patch.yml` 的路由上更正:
```yaml
-llm-pi-ai:
- providers:
- my-gateway:
- apiKeyEnv: GATEWAY_API_KEY
- api: openai-completions
- baseURL: https://gateway.example/v1
- compat:
- supportsDeveloperRole: false
- maxTokensField: max_tokens
- models:
- - id: my-model
+- id: llm-pi-ai
+ config:
+ providers:
+ my-gateway:
+ apiKeyEnv: GATEWAY_API_KEY
+ api: openai-completions
+ baseURL: https://gateway.example/v1
+ compat:
+ supportsDeveloperRole: false
+ maxTokensField: max_tokens
+ models:
+ - id: my-model
```
路由的 `compat` 是其模型的默认值,模型自身的则逐字段胜出,因此更正某一个模型无需重述整条路由:
@@ -187,7 +195,7 @@ llm-pi-ai:
- **获取可用模型提示既没有 `data` 数组也没有 `models` 对象**:端点返回的列表格式不在探测的读取范围内。请手动输入模型。
- **密钥与地址都正确,网关却拒绝每一个请求**:它的请求形状与 OpenAI 不同。先在路由上设 `compat.supportsDeveloperRole: false` 与 `compat.maxTokensField: max_tokens`。
- **只有推理模型失败**:pi-ai 把它们的系统提示词以 `developer` 角色发出,而网关拒绝该角色。设 `compat.supportsDeveloperRole: false`。
-- **手动录入的模型没有推理等级菜单**:该模型没有声明任何等级。在 `settings.yaml` 中给该模型加上 `reasoningEfforts`。
+- **手动录入的模型没有推理等级菜单**:该模型没有声明任何等级。在 `cordis.patch.yml` 中给该模型加上 `reasoningEfforts`。
- **`off` 无法让 DeepSeek 模型停止思考**:留空的 `off` 不发送任何推理字段,默认思考的端点就继续思考。请在模型或路由上设置 `compat.thinkingFormat: deepseek`。
- **某个 compat 开关因没有值而被拒绝**:冒号后什么都没写。给它一个值,或删掉该键以沿用已安装 catalog 的值。
- **图片在发送前被拒绝**:该模型未声明图片模态。请给自定义提供商的模型加上 `input: [text, image]`;在 DeepSeek 自身的路由上,请从配置的目录中选择支持图片的条目(默认为 `deepseek-flash`),并确认网关提供该模型且支持图片输入。
diff --git a/packages/api/settings-controller/src/index.ts b/packages/api/settings-controller/src/index.ts
index 372f4ddf75..34aebd87a5 100644
--- a/packages/api/settings-controller/src/index.ts
+++ b/packages/api/settings-controller/src/index.ts
@@ -11,7 +11,7 @@ import { Context } from '@deepseek-ai/cordis'
import {
openNativeTextFile,
} from '@deepseek-ai/dsh-native-command'
-import type { SettingsDescriptor, SettingsPathOp, SettingsProvider } from '@deepseek-ai/dsh-settings'
+import type { SettingsDescriptor, SettingsPathOp, SettingsForms } from '@deepseek-ai/dsh-settings'
import type {
SettingsDescribeValue, SettingsNamespaceView, SettingsPathOpView,
} from '@deepseek-ai/dsh-settings/types'
@@ -48,6 +48,7 @@ export interface SettingsControllerInternals {
function namespaceView(descriptor: SettingsDescriptor): SettingsNamespaceView {
return {
ns: String(descriptor.ns),
+ autoGenerate: descriptor.autoGenerate,
schema: descriptor.schema as JsonValue,
value: descriptor.value as JsonValue,
...descriptor.base === undefined ? {} : { base: descriptor.base as JsonValue },
@@ -98,7 +99,7 @@ export class SettingsController extends TypertRemoteService {
const settings = this.provider()
return {
writable: settings.writable,
- hasDocument: settings.documentPath !== undefined,
+ hasDocument: true,
namespaces: settings.describe({ redactSecrets: true }).map(namespaceView),
}
}
@@ -166,16 +167,13 @@ export class SettingsController extends TypertRemoteService {
async openSettingsDocument(signal: AbortSignal): Promise {
const settings = this.provider()
if (isAborted(signal)) throw new RemoteError('gateway/cancelled', 'settings document open was aborted', {})
- let path: string | undefined
+ let path: string
try {
path = await settings.prepareDocument()
} catch (error: unknown) {
if (isAborted(signal)) throw new RemoteError('gateway/cancelled', 'settings document preparation was aborted', {})
throw new RemoteError('gateway/internal', `settings document preparation failed: ${messageOf(error)}`, {}, { cause: error })
}
- if (path === undefined) {
- throw new RemoteError('gateway/internal', 'settings provider has no local document to open', {})
- }
if (isAborted(signal)) throw new RemoteError('gateway/cancelled', 'settings document open was aborted', {})
try {
await this.openTextFile(path, signal)
@@ -215,12 +213,12 @@ export class SettingsController extends TypertRemoteService {
}
/** Resolve the optional provider or report how to supply it. */
- private provider(): SettingsProvider {
+ private provider(): SettingsForms {
const settings = this.ctx.get('settings')
if (settings === undefined) {
throw new RemoteError(
'gateway/internal',
- 'settings service is absent: this deployment does not mount a settings provider (e.g. @deepseek-ai/dsh-settings-file) in its composition',
+ 'settings service is absent: mount @deepseek-ai/dsh-settings with @deepseek-ai/dsh-config-editor in the profile composition',
{},
)
}
diff --git a/packages/api/settings-controller/tests/settings-controller.host.spec.ts b/packages/api/settings-controller/tests/settings-controller.host.spec.ts
index 6c30cd6e6d..6d8b867eea 100644
--- a/packages/api/settings-controller/tests/settings-controller.host.spec.ts
+++ b/packages/api/settings-controller/tests/settings-controller.host.spec.ts
@@ -1,330 +1,106 @@
-import { describe, expect, it, vi } from 'vitest'
+/** Remote form operations over real Cordis profile configuration. */
+import { describe, expect, it, onTestFinished, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
-import z from '@deepseek-ai/schemastery'
-import type { SettingsDescriptor } from '@deepseek-ai/dsh-settings'
import { remoteErrorOf, remoteMethods } from '@deepseek-ai/dsh-typert-protocol'
import SettingsController from '../src/index.ts'
-import { MemorySettings } from '../../../settings/settings/tests/memory.ts'
+import { configurationFixture } from '../../../settings/settings/tests/configuration-fixture.ts'
-const NS = 'ui-test'
-
-const Profile = z.object({
- preference: z.union(['light', 'dark']).default('light'),
- apiKey: z.string().role('secret'),
-})
-
-/** A provider that reports a local document, for the `hasDocument` fact. */
-class DocumentSettings extends MemorySettings {
- override get documentPath(): string | undefined {
- return '/deployment/settings.yaml'
- }
+async function boot() {
+ const fixture = await configurationFixture()
+ await fixture.ctx.plugin(SettingsController)
+ return { ...fixture, controller: fixture.ctx.settingsController }
}
-/** A provider whose read forgets the namespace its write just committed. */
-class VanishingSettings extends MemorySettings {
- override describe(): SettingsDescriptor[] {
- return []
- }
-}
-
-/**
- * A provider whose descriptor omits the secret-slot list. `secrets` is optional
- * on the descriptor, so a foreign provider may leave it out even under
- * `redactSecrets`, and the view still has to declare an empty list.
- */
-class SlotlessSettings extends MemorySettings {
- override describe(): SettingsDescriptor[] {
- return [{
- ns: NS,
- schema: Profile.toJSON(),
- value: { preference: 'light' },
- applies: 'live',
- revision: 0,
- } as unknown as SettingsDescriptor]
- }
-}
-
-/** A provider that refuses every write the way a read-only backing store would. */
-class RefusingSettings extends MemorySettings {
- override mutate(): Promise {
- return Promise.reject(new Error('settings are read-only in this deployment'))
- }
-}
-
-/** A provider that refuses with a bare string, the way some storage clients do. */
-class LiteralRefusingSettings extends MemorySettings {
- override async mutate(): Promise {
- throw 'the document is locked'
- }
-}
-
-async function boot(
- provider: typeof MemorySettings = MemorySettings,
- options: { doc?: Record; base?: { preference: 'light' | 'dark' } } = {},
-): Promise<{ controller: SettingsController; ctx: Context }> {
- const ctx = new Context()
- await ctx.plugin(provider, options.doc === undefined ? {} : { doc: options.doc })
- ctx.settings.register(NS, Profile, options.base === undefined ? {} : { base: options.base })
- await ctx.plugin(SettingsController)
- return { controller: ctx.settingsController, ctx }
-}
-
-describe('the settings Remote namespace a configuration page calls', () => {
- it('publishes the settings namespace from its own service key', async () => {
- const { controller } = await boot()
- expect(controller.typertRemote.serviceKey).toBe('settingsController')
+describe('settings Remote', () => {
+ it('publishes form operations and the credentials namespace', async () => {
+ const { ctx, controller } = await boot()
expect(controller.typertRemote.namespace).toBe('settings')
- expect(remoteMethods(controller)).toEqual([
- { method: 'describe', invocation: { kind: 'direct' } },
- { method: 'update', invocation: { kind: 'direct' } },
- { method: 'replace', invocation: { kind: 'direct' } },
- { method: 'mutate', invocation: { kind: 'direct' } },
- { method: 'openSettingsDocument', invocation: { kind: 'direct' } },
+ expect(remoteMethods(controller).map(method => method.method)).toEqual([
+ 'describe', 'update', 'replace', 'mutate', 'openSettingsDocument',
])
- })
-
- it('reports the actionable configuration error while no settings provider is mounted', async () => {
- const ctx = new Context()
- await ctx.plugin(SettingsController)
- const calls: Array<() => unknown> = [
- () => ctx.settingsController.describe(),
- () => ctx.settingsController.update('ui-test', {}, undefined),
- () => ctx.settingsController.replace('ui-test', {}, undefined),
- () => ctx.settingsController.mutate('ui-test', [], undefined),
- () => ctx.settingsController.openSettingsDocument(new AbortController().signal),
- ]
- for (const call of calls) {
- const failure = await Promise.resolve().then(call).catch((error: unknown) => error)
- expect(remoteErrorOf(failure)).toMatchObject({
- code: 'gateway/internal',
- message: 'settings service is absent: this deployment does not mount a settings provider (e.g. @deepseek-ai/dsh-settings-file) in its composition',
- details: {},
- })
- }
- })
-
- it('mounts the credentials namespace beside its own', async () => {
- const ctx = new Context()
- await ctx.plugin(MemorySettings)
- ctx.settings.register(NS, Profile)
- const fiber = ctx.plugin(SettingsController)
- await fiber.await()
expect(ctx.get('credentialsController')).toBeDefined()
- await fiber.dispose()
- expect(ctx.get('settingsController')).toBeUndefined()
- expect(ctx.get('credentialsController')).toBeUndefined()
})
- it('describes every namespace redacted, with the deployment facts around them', async () => {
- const { controller } = await boot(DocumentSettings, { doc: { 'ui-test': { apiKey: 'sk-stored' } } })
- const value = controller.describe()
- expect(value).toMatchObject({ writable: true, hasDocument: true })
- const [view] = value.namespaces
- expect(view?.ns).toBe('ui-test')
- // The secret never rides; its slot reports only that one is stored.
- expect(JSON.stringify(value)).not.toContain('sk-stored')
- expect(view?.secrets).toEqual([{ path: ['apiKey'], set: true }])
- // Redaction removes the field rather than replacing it, so the layer that
- // stored a secret comes back empty instead of carrying a placeholder.
- expect(view?.user).toEqual({})
+ it('redacts every described layer and applies mutations through the consumer', async () => {
+ const { controller, ctx } = await boot()
+ const describe = controller.describe()
+ expect(describe).toMatchObject({ writable: true, hasDocument: true })
+ expect(JSON.stringify(describe)).not.toContain('private')
+ const model = describe.namespaces.find(row => row.ns === 'default-model')!
+ const saved = await controller.mutate(model.ns, [{ op: 'set', path: ['model'], value: 'next' }], model.revision)
+ expect(saved.value).toEqual({ provider: 'test', model: 'next' })
+ expect(ctx.agentDefaultModel.currentSelection().model).toBe('next')
+ await expect(controller.update(model.ns, { model: 'stale' }, model.revision)).rejects.toMatchObject({ code: 'settings/conflict' })
+ await expect(controller.update('first', { count: 0 }, undefined)).rejects.toMatchObject({ code: 'settings/rejected' })
+ await expect(controller.update('', {}, undefined)).rejects.toMatchObject({ code: 'gateway/bad-request' })
+ await expect(controller.update('missing', {}, undefined)).rejects.toMatchObject({ code: 'settings/rejected' })
+ expect((await controller.replace('default-model', {}, undefined)).value).toEqual({ provider: 'test', model: 'original' })
})
- it('reports a read-only provider and omits the layers it has none of', async () => {
- const { controller } = await boot(class extends MemorySettings {
- override get writable(): boolean {
- return false
- }
- })
- const value = controller.describe()
- expect(value).toMatchObject({ writable: false, hasDocument: false })
- const [view] = value.namespaces
- // No composition base was declared and no user section is stored, so
- // neither optional layer appears at all.
- expect(view && 'base' in view).toBe(false)
- expect(view && 'user' in view).toBe(false)
- })
-
- it('declares an empty slot list when the provider names no secrets', async () => {
- const { controller } = await boot(SlotlessSettings)
- const [view] = controller.describe().namespaces
- expect(view?.secrets).toEqual([])
- })
-
- it('carries the composition base layer when the registrant declared one', async () => {
- const { controller } = await boot(MemorySettings, { base: { preference: 'dark' } })
- const [view] = controller.describe().namespaces
- expect(view?.base).toEqual({ preference: 'dark' })
- })
-
- it('applies path-addressed edits and answers with the namespace it just wrote', async () => {
- const { controller } = await boot()
- const view = await controller.mutate('ui-test', [{ op: 'set', path: ['preference'], value: 'dark' }], undefined)
- expect(view).toMatchObject({ ns: 'ui-test', user: { preference: 'dark' } })
- expect(view.revision).toBeGreaterThan(0)
- })
-
- it('supports merge updates and wholesale replacement on the Remote namespace', async () => {
- const { controller } = await boot(MemorySettings, {
- doc: { 'ui-test': { preference: 'dark', apiKey: 'sk-stored' } },
- })
- const updated = await controller.update('ui-test', { preference: 'light' }, undefined)
- expect(updated.user).toEqual({ preference: 'light' })
- expect(updated.secrets).toEqual([{ path: ['apiKey'], set: true }])
-
- const replaced = await controller.replace('ui-test', {}, updated.revision)
- expect(replaced.value).toEqual({ preference: 'light' })
- expect(replaced.user).toEqual({})
- expect(replaced.secrets).toEqual([{ path: ['apiKey'], set: false }])
- })
-
- it('refuses a stale write as settings/conflict carrying both revisions', async () => {
- const { controller } = await boot()
- const held = controller.describe().namespaces[0]!.revision
- await controller.mutate('ui-test', [{ op: 'set', path: ['preference'], value: 'dark' }], held)
- const failure = await controller
- .mutate('ui-test', [{ op: 'set', path: ['preference'], value: 'light' }], held)
- .catch((error: unknown) => error)
- const { code, details } = remoteErrorOf(failure) ?? {}
- expect(code).toBe('settings/conflict')
- expect(details).toMatchObject({ ns: 'ui-test', expected: held })
- })
-
- it('answers a malformed namespace exactly as an unregistered one', async () => {
- const { controller } = await boot()
- for (const ns of ['Not A Namespace', 'unregistered']) {
- const failure = await controller.mutate(ns, [{ op: 'unset', path: ['preference'] }], undefined)
- .catch((error: unknown) => error)
- expect(remoteErrorOf(failure)).toMatchObject({
- code: 'settings/rejected',
- details: { ns },
- })
- }
- })
-
- it('reports an empty namespace as bad-request', async () => {
- const { controller } = await boot()
- for (const call of [
- () => controller.update('', {}, undefined),
- () => controller.replace('', {}, undefined),
- () => controller.mutate('', [], undefined),
- ]) {
- const failure = await call().catch((error: unknown) => error)
- expect(remoteErrorOf(failure)).toMatchObject({ code: 'gateway/bad-request' })
- }
- })
-
- it('reports a refused write as settings/rejected carrying the seam message', async () => {
- const { controller } = await boot(RefusingSettings)
- const failure = await controller.mutate('ui-test', [{ op: 'unset', path: ['preference'] }], undefined)
- .catch((error: unknown) => error)
- const { code, message } = remoteErrorOf(failure) ?? {}
- expect(code).toBe('settings/rejected')
- expect(message).toContain('read-only in this deployment')
- })
-
- it('stringifies a refusal that is not an Error', async () => {
- const { controller } = await boot(LiteralRefusingSettings)
- const failure = await controller.mutate('ui-test', [{ op: 'unset', path: ['preference'] }], undefined)
- .catch((error: unknown) => error)
- expect(remoteErrorOf(failure)?.message).toBe('the document is locked')
- })
-
- it('reports a namespace disposed between the write and its read-back', async () => {
- const { controller } = await boot(VanishingSettings)
- const failure = await controller.mutate('ui-test', [{ op: 'set', path: ['preference'], value: 'dark' }], undefined)
- .catch((error: unknown) => error)
- const { code, message } = remoteErrorOf(failure) ?? {}
- expect(code).toBe('gateway/internal')
- expect(message).toContain('was disposed after the mutate')
- })
-
- it('prepares and opens the provider-owned settings document', async () => {
+ it('reports an absent Config form service', async () => {
const ctx = new Context()
- await ctx.plugin(DocumentSettings)
- const prepare = vi.spyOn(ctx.settings, 'prepareDocument').mockResolvedValue('/tmp/settings.yaml')
- const openTextFile = vi.fn((_path: string, _signal: AbortSignal) => Promise.resolve())
- const controller = new SettingsController(ctx, { openTextFile })
- const signal = new AbortController().signal
-
- await expect(controller.openSettingsDocument(signal)).resolves.toEqual({ opened: true })
- expect(prepare).toHaveBeenCalledOnce()
- expect(openTextFile).toHaveBeenCalledWith('/tmp/settings.yaml', signal)
+ onTestFinished(() => ctx.fiber.dispose())
+ const controller = new SettingsController(ctx)
+ expect(() => controller.describe()).toThrow('settings service is absent')
+ const failure = await controller.update('missing', {}, undefined).catch((error: unknown) => error)
+ expect(remoteErrorOf(failure)?.code).toBe('gateway/internal')
})
- it('preserves settings-document absence, failure, and cancellation', async () => {
- const absent = await boot()
- const missingDocument = absent.controller.openSettingsDocument(new AbortController().signal)
- await expect(missingDocument).rejects.toMatchObject({ code: 'gateway/internal' })
- await expect(missingDocument).rejects.toThrow('no local document')
-
- const failed = await boot(DocumentSettings)
- vi.spyOn(failed.ctx.settings, 'prepareDocument').mockRejectedValue(new Error('read failed'))
- const failedRead = failed.controller.openSettingsDocument(new AbortController().signal)
- await expect(failedRead).rejects.toMatchObject({ code: 'gateway/internal' })
- await expect(failedRead).rejects.toThrow('read failed')
-
- const cancelled = new AbortController()
- cancelled.abort(new Error('cancelled'))
- const prepare = vi.spyOn(failed.ctx.settings, 'prepareDocument')
- prepare.mockClear()
- await expect(failed.controller.openSettingsDocument(cancelled.signal))
- .rejects.toMatchObject({ code: 'gateway/cancelled' })
- expect(prepare).not.toHaveBeenCalled()
- })
-
- it('does not open a settings document cancelled during preparation', async () => {
- const ctx = new Context()
- await ctx.plugin(DocumentSettings)
- const prepared = Promise.withResolvers()
- vi.spyOn(ctx.settings, 'prepareDocument').mockReturnValue(prepared.promise)
- const openTextFile = vi.fn((_path: string, _signal: AbortSignal) => Promise.resolve())
+ it('opens the profile patch and contains opener failure and cancellation', async () => {
+ const { ctx, profile } = await configurationFixture()
+ const openTextFile = vi.fn(async (_path: string, _signal: AbortSignal) => {})
const controller = new SettingsController(ctx, { openTextFile })
const abort = new AbortController()
+ await controller.openSettingsDocument(abort.signal)
+ expect(openTextFile).toHaveBeenCalledWith(profile.patchPath, abort.signal)
+ openTextFile.mockRejectedValueOnce(new Error('no editor'))
+ await expect(controller.openSettingsDocument(abort.signal)).rejects.toMatchObject({ code: 'gateway/internal' })
+ abort.abort()
+ await expect(controller.openSettingsDocument(abort.signal)).rejects.toMatchObject({ code: 'gateway/cancelled' })
+ })
+ it('does not open a patch cancelled while preparation is pending', async () => {
+ const { ctx } = await configurationFixture()
+ const prepared = Promise.withResolvers()
+ vi.spyOn(ctx.settings, 'prepareDocument').mockReturnValue(prepared.promise)
+ const openTextFile = vi.fn(async (_path: string, _signal: AbortSignal) => {})
+ const controller = new SettingsController(ctx, { openTextFile })
+ const abort = new AbortController()
const opening = controller.openSettingsDocument(abort.signal)
- abort.abort(new Error('cancelled'))
- prepared.resolve('/tmp/settings.yaml')
-
+ abort.abort()
+ prepared.resolve('/tmp/cordis.patch.yml')
await expect(opening).rejects.toMatchObject({ code: 'gateway/cancelled' })
expect(openTextFile).not.toHaveBeenCalled()
})
- it('maps native settings-document opener failures', async () => {
- const ctx = new Context()
- await ctx.plugin(DocumentSettings)
- vi.spyOn(ctx.settings, 'prepareDocument').mockResolvedValue('/tmp/settings.yaml')
- const controller = new SettingsController(ctx, {
- openTextFile: () => Promise.reject(new Error('no default editor')),
- })
+})
- await expect(controller.openSettingsDocument(new AbortController().signal))
- .rejects.toMatchObject({ code: 'gateway/internal', message: 'path open failed: no default editor' })
- })
+it('omits absent optional descriptor fields and contains primitive write failures', async () => {
+ const { ctx, controller } = await boot()
+ vi.spyOn(ctx.settings, 'describe').mockReturnValue([{ ns: 'first' as never, schema: {}, value: {}, autoGenerate: true, applies: 'live', revision: 0 }])
+ expect(controller.describe().namespaces[0]).toEqual({ ns: 'first', schema: {}, value: {}, autoGenerate: true, applies: 'live', revision: 0, secrets: [] })
+ vi.spyOn(ctx.settings, 'update').mockRejectedValueOnce('storage unavailable')
+ await expect(controller.update('first', {}, undefined)).rejects.toMatchObject({ code: 'settings/rejected' })
+})
- it('classifies cancellation while preparing or opening the settings document', async () => {
- const preparing = new Context()
- await preparing.plugin(DocumentSettings)
- const prepareAbort = new AbortController()
- vi.spyOn(preparing.settings, 'prepareDocument').mockImplementation(async () => {
- prepareAbort.abort(new Error('cancelled'))
- throw new Error('preparation stopped')
- })
- const preparingController = new SettingsController(preparing)
- await expect(preparingController.openSettingsDocument(prepareAbort.signal))
- .rejects.toMatchObject({ code: 'gateway/cancelled' })
+it('reports a form removed after its write committed', async () => {
+ const { ctx, controller } = await boot()
+ const entry = ctx.configEditor.entries().find(row => row.options.id === 'first')!
+ vi.spyOn(ctx.settings, 'update').mockImplementation(async () => { entry.parent.remove(entry.options.id) })
+ await expect(controller.update('first', {}, undefined)).rejects.toMatchObject({ code: 'gateway/internal' })
+})
- const opening = new Context()
- await opening.plugin(DocumentSettings)
- vi.spyOn(opening.settings, 'prepareDocument').mockResolvedValue('/tmp/settings.yaml')
- const openAbort = new AbortController()
- const openingController = new SettingsController(opening, {
- openTextFile: async () => {
- openAbort.abort(new Error('cancelled'))
- throw new Error('opening stopped')
- },
- })
- await expect(openingController.openSettingsDocument(openAbort.signal))
- .rejects.toMatchObject({ code: 'gateway/cancelled' })
- })
-
+it('classifies document preparation failures and cancellation during opening', async () => {
+ const { ctx } = await configurationFixture({ hmr: false })
+ let abort = new AbortController()
+ const controller = new SettingsController(ctx, { openTextFile: async () => {
+ abort.abort(); throw new Error('opening cancelled')
+ } })
+ const prepare = vi.spyOn(ctx.settings, 'prepareDocument')
+ prepare.mockRejectedValueOnce(new Error('unreadable profile'))
+ await expect(controller.openSettingsDocument(abort.signal)).rejects.toMatchObject({ code: 'gateway/internal' })
+ prepare.mockImplementationOnce(async () => { abort.abort(); throw new Error('preparation cancelled') })
+ await expect(controller.openSettingsDocument(abort.signal)).rejects.toMatchObject({ code: 'gateway/cancelled' })
+ abort = new AbortController()
+ await expect(controller.openSettingsDocument(abort.signal)).rejects.toMatchObject({ code: 'gateway/cancelled' })
})
diff --git a/packages/boot/README.i18n.yaml b/packages/boot/README.i18n.yaml
index 824ff1ff5a..f6ad94fed7 100644
--- a/packages/boot/README.i18n.yaml
+++ b/packages/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/README.md
-README.md: f7844725e480aa0f1a6247168882d474301db287
-README.zh.md: 9ba865baec3abaa410bf70fe18418729c8af6851
+README.md: 826d74a8af78048e8dc90bfc04afee78a63e35fd
+README.zh.md: c16768dc3510394379ed25d5514c12caa9c0ac9a
diff --git a/packages/boot/README.md b/packages/boot/README.md
index f7844725e4..826d74a8af 100644
--- a/packages/boot/README.md
+++ b/packages/boot/README.md
@@ -25,6 +25,7 @@ The boot group launches profile applications and manages their installed composi
| [`app-boot`](app-boot/README.md) | Boots a dsh app from a `cordis.yml`: loads `.env`, applies profile and patch layers, and reports startup failures clearly | (library for the bins) |
| [`cmdline`](cmdline/README.md) | Lets the app own its flags, `--help`, and exit code; passes everything after the launcher's flags through verbatim | `cmdlineArgs`, `appExit` |
| [`hmr`](hmr/README.md) | Coordinates module and configuration reloads with package mutations | `hmr` |
+| [`config-editor`](config-editor/README.md) | Persists active profile configuration through Loader reconciliation | `configEditor` |
| [`plugin-manager`](plugin-manager/README.md) | Manages current-profile plugins and bundle packages through shared CLI operations | `pluginManager` |
diff --git a/packages/boot/README.zh.md b/packages/boot/README.zh.md
index 9ba865baec..c16768dc35 100644
--- a/packages/boot/README.zh.md
+++ b/packages/boot/README.zh.md
@@ -25,6 +25,7 @@ boot 组负责启动 profile 应用并管理其已安装组合。`app-boot` 解
| [`app-boot`](app-boot/README.zh.md) | 从 `cordis.yml` 启动 dsh 应用:加载 `.env`、应用 profile 与 patch 层,并清晰报告启动失败 | (供各 bin 使用的库) |
| [`cmdline`](cmdline/README.zh.md) | 让应用持有自己的 flag、`--help` 与退出码;启动器自身 flag 之后的一切原样传入 | `cmdlineArgs`、`appExit` |
| [`hmr`](hmr/README.zh.md) | 协调模块与配置重载,并与包修改互斥执行 | `hmr` |
+| [`config-editor`](config-editor/README.zh.md) | 通过 Loader 应用并持久化当前 profile 配置 | `configEditor` |
| [`plugin-manager`](plugin-manager/README.zh.md) | 通过共享 CLI 操作管理当前 profile 插件与组合包 | `pluginManager` |
diff --git a/packages/boot/app-boot/README.i18n.yaml b/packages/boot/app-boot/README.i18n.yaml
index 2c707110a8..13dd392418 100644
--- a/packages/boot/app-boot/README.i18n.yaml
+++ b/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: 55cd3c9c1a6e58e73204ff55da3d898b49de84bd
-README.zh.md: 7ff9f75d200f7d56555b9cf543a9b5d2aefbec9b
+README.md: 89dca0593489885bf2a59851e641d7238f2d66e2
+README.zh.md: 78c07a21e79d47fbf9b980509bd9a42d17f4fd11
diff --git a/packages/boot/app-boot/README.md b/packages/boot/app-boot/README.md
index 55cd3c9c1a..89dca05934 100644
--- a/packages/boot/app-boot/README.md
+++ b/packages/boot/app-boot/README.md
@@ -81,7 +81,7 @@ Use `readPluginMeta(specifier, parentURL)` or `ctx.pluginPackages.metaOf(specifi
### Startup and reload failures
-Profile reconciliation returns diagnostics for unchanged inactive entries without failing an unrelated mutation. A new inactive entry, a changed configuration or fiber, or a changed diagnostic fails reconciliation; removed fibers must still finish disposal. Explicit enablement targets must activate even when their failure predates the operation. Successful reconciliation returns after lifecycle settlement and diagnostic checks; volatile-only entry changes are committed by Loader during the update.
+Profile reconciliation returns diagnostics for unchanged inactive entries without failing an unrelated mutation. A new inactive entry, a changed configuration or fiber, or a changed diagnostic fails reconciliation; removed fibers must still finish disposal. Explicit enablement targets must activate even when their failure predates the operation. Successful reconciliation emits `app-boot/config-reload` after lifecycle settlement and diagnostic checks, including programmatic updates without HMR. The event carries no diff or parsed config. Successful reconciliation returns after lifecycle settlement and diagnostic checks; volatile-only entry changes are committed by Loader during the update.
After the Loader settles, app-boot warns when only optional entries are inactive. If an enabled required entry cannot activate, `boot()` rejects with `StartupError` after disposal. An independently owned logger exporter retains warning and error records through asynchronous disposal and is released before `boot()` settles. Its message groups all failed plugins and pending services, marks required entries, and retains original stacks, nested causes, and aggregate members. The CLI prints that message once and saves [full startup diagnostics](../../../apps/cli/reference/README.md#startup-diagnostics) before exiting with code 1; unrelated exceptions retain their normal stack output. In the table, stopping startup means disposing any mounted plugins and exiting nonzero without reporting readiness; continuing keeps successful plugins running. Later configuration HMR does not repeat the required-startup audit and does not roll back the whole update.
diff --git a/packages/boot/app-boot/README.zh.md b/packages/boot/app-boot/README.zh.md
index 7ff9f75d20..78c07a21e7 100644
--- a/packages/boot/app-boot/README.zh.md
+++ b/packages/boot/app-boot/README.zh.md
@@ -81,7 +81,7 @@ profile 是同一套 dsh 安装提供不同应用界面的方式:`web`、`head
### 启动与重载失败
-profile 重载返回未变化的已有故障诊断,不让无关修改因此失败。新增未激活条目、配置或 fiber 变化、诊断变化都会使重载失败;被移除的 fiber 仍须完成释放。显式启用的目标必须成功激活,即使它的故障早于本次操作。 成功重载在生命周期结束及诊断检查通过后返回;仅 volatile 的条目变化由 Loader 在更新过程中提交。
+profile 重载返回未变化的已有故障诊断,不让无关修改因此失败。新增未激活条目、配置或 fiber 变化、诊断变化都会使重载失败;被移除的 fiber 仍须完成释放。显式启用的目标必须成功激活,即使它的故障早于本次操作。成功重载在生命周期结束及诊断检查通过后发出 `app-boot/config-reload`,包括未启用 HMR 时的程序化更新。事件不携带 diff 或解析后的配置。 成功重载在生命周期结束及诊断检查通过后返回;仅 volatile 的条目变化由 Loader 在更新过程中提交。
Loader 结算后,app-boot 在仅 optional 条目未激活时输出警告。如果已启用的 required 条目无法激活,`boot()` 会在释放资源后以 `StartupError` 拒绝。独立管理生命周期的 logger exporter 会保留异步资源释放期间的警告和错误记录,并在 `boot()` 结算前释放。其消息分组列出所有失败插件和等待的服务,标记 required 条目,并保留原始堆栈、嵌套原因和聚合错误成员。CLI 仅输出该消息一次,并在保存[完整启动诊断](../../../apps/cli/reference/README.zh.md#startup-diagnostics)后以退出码 1 结束;其他异常保留正常堆栈输出。表中的“终止启动”指释放已挂载插件并以非零码退出,不报告就绪;“继续”指保留成功运行的插件。后续配置 HMR 不会再次执行 required 启动审计,也不会回滚整个更新。
diff --git a/packages/boot/app-boot/src/index.ts b/packages/boot/app-boot/src/index.ts
index 8688f96667..dd1669b5e0 100644
--- a/packages/boot/app-boot/src/index.ts
+++ b/packages/boot/app-boot/src/index.ts
@@ -33,6 +33,15 @@ declare module '@deepseek-ai/cordis' {
/** Harness-home path resolver available to Loader `!!js` config expressions. */
dshHomePath?: typeof dshHomePath
}
+
+ interface Events {
+ /**
+ * Profile patches were reconciled into the running Loader tree: every entry update settled and no new
+ * inactive entry was introduced. Carries no diff; listeners re-read Loader entries.
+ * @mode emit
+ */
+ 'app-boot/config-reload'(): void
+ }
}
export {
@@ -274,6 +283,7 @@ export async function reconcileProfilePatches(
for (const [index, result] of results.entries()) {
if (result.status === 'rejected' && !previousFibers[index]?.failed) throw result.reason
}
+ ctx.emit('app-boot/config-reload')
return failures.map(inactiveDiagnostic)
}
diff --git a/packages/settings/settings-file/README.i18n.yaml b/packages/boot/config-editor/README.i18n.yaml
similarity index 55%
rename from packages/settings/settings-file/README.i18n.yaml
rename to packages/boot/config-editor/README.i18n.yaml
index 795c07205e..94114937e7 100644
--- a/packages/settings/settings-file/README.i18n.yaml
+++ b/packages/boot/config-editor/README.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 packages/settings/settings-file/README.md
-README.md: ea578a1739e1bd5d7b7f47be596998b124337ed5
-README.zh.md: 668d771bd64738701de87ca28066234284941873
+# pnpm run verify-translation-pairing --write packages/boot/config-editor/README.md
+README.md: b2dd0fe5bce7e2cef63a6dfc7437f3ef7066df9a
+README.zh.md: 35823f52ea12c3e073ece8fd3d33698007ec8506
diff --git a/packages/boot/config-editor/README.md b/packages/boot/config-editor/README.md
new file mode 100644
index 0000000000..b2dd0fe5bc
--- /dev/null
+++ b/packages/boot/config-editor/README.md
@@ -0,0 +1,79 @@
+---
+description: "Save active profile configuration and apply it through Loader."
+kind: "package-reference"
+---
+
+# @deepseek-ai/dsh-config-editor
+
+English | [中文](README.zh.md)
+
+## Summary
+
+Save plugin configuration in the active profile’s patch and apply it immediately. Writes validate the complete candidate before touching disk and serialize with profile changes and HMR. Invalid values and higher-layer overrides leave the file unchanged.
+
+## Table of Contents
+
+- [Use this package](#use-this-package)
+- [Understand the implementation](#understand-the-implementation)
+- [Further Exploration](#further-exploration)
+- [Model Experience](#model-experience)
+- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
+- [Dev Note](#dev-note)
+
+
+## Use this package
+
+Mount this service in a profile application with Loader and `profileContext`. It has no configuration fields.
+
+```yaml
+- id: config-editor
+ name: '@deepseek-ai/dsh-config-editor'
+```
+
+Use [settings](../../settings/settings/README.md) for forms restricted to live fields. Callers that edit complete configuration can use `ctx.configEditor.edit()`; ordinary fields retain Loader’s normal lifecycle.
+
+
+## Understand the implementation
+
+
+Implementation internals — click to expand
+
+The [editor](src/index.ts) reconciles external changes before deriving a candidate, locks the profile manifest alongside other profile operations, and atomically replaces its config override. It preserves YAML comments and `!!js` expressions outside replaced values. A failed reconciliation restores the prior document and reloads the prior patches.
+
+No invariant companion is published: the editor keeps no independent configuration projection. Loader and the persisted profile patch own configuration state.
+
+
+
+
+## Further Exploration
+
+- [Profile loading](../app-boot/README.md) — patch composition.
+- [HMR](../hmr/README.md) — reload coordination.
+- [Settings](../../settings/settings/README.md) — schema-derived forms.
+
+
+## Model Experience
+
+Indirectly, through configuration values consumed by model-facing plugins.
+
+#### KV Cache effect
+
+Consumers that change request prefixes determine cache effects.
+
+## Known Limitations and Deferred Work
+
+
+
+- Edits target the active profile patch. Home patches and command-line overlays are read for precedence but are not write targets.
+- A complete config override preserves ordinary fields but pins their current raw values at the profile layer.
+- Only uniquely addressed entries owned by the profile’s root Include are editable.
+
+
+### Dev Note
+
+
+Working context for maintainers — click to expand
+
+None.
+
+
diff --git a/packages/boot/config-editor/README.zh.md b/packages/boot/config-editor/README.zh.md
new file mode 100644
index 0000000000..35823f52ea
--- /dev/null
+++ b/packages/boot/config-editor/README.zh.md
@@ -0,0 +1,79 @@
+---
+description: "保存当前 profile 配置,并通过 Loader 应用。"
+kind: "package-reference"
+---
+
+# @deepseek-ai/dsh-config-editor
+
+[English](README.md) | 中文
+
+## 概述
+
+将插件配置保存到当前 profile 的 patch 并立即应用。写入在改动磁盘前验证完整候选值,并与 profile 更改及 HMR 串行执行。无效值和更高层覆盖不会改动文件。
+
+## 目录
+
+- [使用此包](#use-this-package)
+- [理解实现](#understand-the-implementation)
+- [进一步探索](#further-exploration)
+- [模型体验](#model-experience)
+- [已知限制与延后工作](#known-limitations-and-deferred-work)
+- [开发备注](#dev-note)
+
+
+## 使用此包
+
+在具有 Loader 和 `profileContext` 的 profile 应用中挂载此服务。它没有配置字段。
+
+```yaml
+- id: config-editor
+ name: '@deepseek-ai/dsh-config-editor'
+```
+
+使用 [settings](../../settings/settings/README.zh.md) 提供只编辑即时字段的表单。编辑完整配置的调用方可使用 `ctx.configEditor.edit()`;普通字段保留 Loader 的正常生命周期。
+
+
+## 理解实现
+
+
+实现内部细节——点击展开
+
+[编辑器](src/index.ts) 在派生候选配置前应用外部更改,与其他 profile 操作共同锁定 profile manifest,并原子替换配置覆盖项。它保留替换值以外的 YAML 注释和 `!!js` 表达式。应用失败时恢复之前的文档并重新加载之前的 patch。
+
+此包不发布 invariant companion:编辑器不维护独立配置投影。Loader 和持久化的 profile patch 拥有配置状态。
+
+
+
+
+## 进一步探索
+
+- [Profile 加载](../app-boot/README.zh.md)——patch 组合。
+- [HMR](../hmr/README.zh.md)——重载协调。
+- [设置](../../settings/settings/README.zh.md)——schema 派生表单。
+
+
+## 模型体验
+
+通过面向模型的插件读取的配置值间接影响模型。
+
+#### KV Cache 影响
+
+改变请求前缀的消费者决定缓存影响。
+
+## 已知限制与延后工作
+
+
+
+- 编辑写入当前 profile patch。Home patch 和命令行 overlay 参与优先级解析,但不作为写入目标。
+- 完整配置覆盖保留普通字段,但会在 profile 层固定其当前原始值。
+- 仅可编辑 profile 根 Include 拥有且可唯一定位的条目。
+
+
+### 开发备注
+
+
+维护者工作上下文——点击展开
+
+无。
+
+
diff --git a/packages/boot/config-editor/package.json b/packages/boot/config-editor/package.json
new file mode 100644
index 0000000000..85ba5938f1
--- /dev/null
+++ b/packages/boot/config-editor/package.json
@@ -0,0 +1,54 @@
+{
+ "name": "@deepseek-ai/dsh-config-editor",
+ "description": "Persist plugin configuration through profile patches and Loader reconciliation",
+ "version": "0.1.6-alpha.2",
+ "publishConfig": {
+ "access": "public"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
+ "directory": "packages/boot/config-editor"
+ },
+ "type": "module",
+ "main": "lib/index.js",
+ "types": "lib/types/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./lib/types/index.d.ts",
+ "default": "./lib/index.js"
+ },
+ "./src/*": "./src/*",
+ "./package.json": "./package.json"
+ },
+ "files": [
+ "lib/index.js",
+ "lib/types/**/*.d.ts"
+ ],
+ "license": "MIT",
+ "peerDependencies": {
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/cordis-plugin-loader": "workspace:^",
+ "@deepseek-ai/dsh-hmr": "workspace:^",
+ "@deepseek-ai/cordis-plugin-include": "workspace:^",
+ "@deepseek-ai/dsh-app-boot": "workspace:^"
+ },
+ "dependencies": {
+ "@deepseek-ai/dsh-atomic-write": "workspace:^",
+ "yaml": "^2.8.1",
+ "js-yaml": "^4.1.0"
+ },
+ "devDependencies": {
+ "@deepseek-ai/cordis": "workspace:^",
+ "@deepseek-ai/cordis-plugin-loader": "workspace:^",
+ "@deepseek-ai/dsh-hmr": "workspace:^",
+ "@deepseek-ai/cordis-plugin-include": "workspace:^",
+ "@types/js-yaml": "^4.0.9",
+ "@deepseek-ai/dsh-app-boot": "workspace:^"
+ },
+ "peerDependenciesMeta": {
+ "@deepseek-ai/dsh-hmr": {
+ "optional": true
+ }
+ }
+}
diff --git a/packages/boot/config-editor/src/index.ts b/packages/boot/config-editor/src/index.ts
new file mode 100644
index 0000000000..8caad69562
--- /dev/null
+++ b/packages/boot/config-editor/src/index.ts
@@ -0,0 +1,145 @@
+/** Profile-owned configuration edits, serialized with Loader hot reload. */
+import { readFile } from 'node:fs/promises'
+import { join } from 'node:path'
+import { isDeepStrictEqual } from 'node:util'
+import { Context, FiberState, Service, resolveConfig } from '@deepseek-ai/cordis'
+import { entryListSchema, type PatchOptions } from '@deepseek-ai/cordis-plugin-include'
+import yaml from 'js-yaml'
+import type { Entry, EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
+import type {} from '@deepseek-ai/dsh-hmr'
+import { composeEntries, loadProfileDirectory, readProfilePatches, reconcileProfilePatches } from '@deepseek-ai/dsh-app-boot'
+import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'
+import { isMap, isSeq, parseDocument, Scalar, visit } from 'yaml'
+
+declare module '@deepseek-ai/cordis' {
+ interface Context {
+ /** Persistent edits to the active profile's plugin configuration. */
+ configEditor: ConfigEditor
+ }
+}
+
+function flatten(rows: EntryOptions[]): EntryOptions[] {
+ return rows.flatMap(row => [row, ...row.group && Array.isArray(row.config) ? flatten(row.config as EntryOptions[]) : []])
+}
+
+/** Persist complete raw configs and apply them through the normal Loader path. */
+export class ConfigEditor extends Service {
+ static inject = ['loader', 'profileContext']
+
+ constructor(private readonly ownerContext: Context) {
+ super(ownerContext, 'configEditor')
+ }
+
+ /** The profile patch edited by this service. */
+ get documentPath(): string { return this.ownerContext.profileContext.patchPath }
+
+ /** Addressable profile rows; nested Includes have independent configuration ownership.
+ * @returns Active entries with unique profile patch ids.
+ */
+ entries(): Entry[] {
+ const candidates = [...this.ownerContext.loader.entries()].filter(entry => entry.parent.tree.ctx.fiber.entry?.id === 'include')
+ const counts = new Map()
+ for (const entry of candidates) counts.set(entry.options.id, (counts.get(entry.options.id) ?? 0) + 1)
+ return candidates.filter(entry => counts.get(entry.options.id) === 1)
+ }
+
+ /** Read inherited and explicit profile values for the active entries.
+ * @returns Detached layer values alongside their Loader entries.
+ */
+ configuration(): Array<{ entry: Entry; inherited: Record; override: Record }> {
+ const profile = this.ownerContext.profileContext
+ const loaded = loadProfileDirectory('dsh', profile.dir, profile.installAnchor)
+ return this.entries().map(entry => ({
+ entry, inherited: this.inherited(entry, loaded),
+ override: structuredClone((loaded.patches.findLast(
+ row => row.id === entry.options.id && row.config !== undefined,
+ )?.config ?? {}) as Record),
+ }))
+ }
+
+ private inherited(entry: Entry, loaded: ReturnType): Record {
+ const patches = loaded.patches.map((patch) => {
+ if (patch.id !== entry.options.id || patch.insert !== undefined) return patch
+ const rest = { ...patch }; Reflect.deleteProperty(rest, 'config')
+ return rest
+ })
+ const row = flatten(composeEntries([...loaded.layers.map(layer => layer.patches), patches])).find(row => row.id === entry.options.id)
+ return structuredClone((row?.config ?? {}) as Record)
+ }
+
+ /** Validate, persist, and reconcile a plugin's next config; ordinary fields keep normal lifecycle rules.
+ * @param entry Current Loader entry, also used to detect replacement during the write.
+ * @param change Derive a raw config from the current entry and its inherited layer.
+ * @returns Fulfillment after Loader reconciliation completes.
+ */
+ async edit(
+ entry: Entry,
+ change: (current: Record, inherited: Record) => Record,
+ ): Promise {
+ const run = async (): Promise => {
+ const path = this.documentPath
+ await withFileLock(join(this.ownerContext.profileContext.dir, 'package.json'), async () => {
+ if (!this.entries().includes(entry) || entry.fiber === undefined) throw new Error('Configuration entry is no longer available')
+ const beforePatches = readProfilePatches('dsh', this.ownerContext.profileContext)
+ await reconcileProfilePatches(this.ownerContext.root, beforePatches, 'dsh')
+ if (!this.entries().includes(entry)) throw new Error('Configuration entry changed during reload')
+ const current = structuredClone((entry.options.config ?? {}) as Record)
+ const inherited = this.inherited(entry, loadProfileDirectory('dsh', this.ownerContext.profileContext.dir, this.ownerContext.profileContext.installAnchor))
+ const next = change(current, inherited)
+ const fiber = entry.fiber
+ if (fiber.state !== FiberState.ACTIVE) throw new Error('Configuration plugin is no longer active')
+ const resolved: unknown = fiber.ctx.waterfall(fiber, 'internal/config', next, () => next)
+ resolveConfig(fiber.runtime as NonNullable, resolved)
+ let before: string
+ try { before = await readFile(path, 'utf8') }
+ catch (error) {
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
+ before = '[]\n'
+ }
+ const document = parseDocument(before, {
+ customTags: [{ tag: 'tag:yaml.org,2002:js', resolve: (value: string) => value }],
+ })
+ if (document.errors[0] !== undefined) throw document.errors[0]
+ if (!isSeq(document.contents)) throw new Error('Profile patch must be a YAML sequence')
+ document.contents.flow = false
+ const index = document.contents.items.findLastIndex((item, index) => isMap(item)
+ && document.getIn([index, 'id']) === entry.options.id && !item.has('insert')
+ && (!item.has('name') || document.getIn([index, 'name']) === entry.options.name))
+ if (isDeepStrictEqual(next, inherited)) {
+ for (let index = document.contents.items.length - 1; index >= 0; index--) {
+ const row = document.contents.items[index]
+ if (!isMap(row) || document.getIn([index, 'id']) !== entry.options.id || row.has('insert')) continue
+ row.delete('config')
+ if (row.items.length === Number(row.has('id')) + Number(row.has('name'))) document.delete(index)
+ }
+ } else if (index < 0) document.add(document.createNode({ id: entry.options.id, name: entry.options.name, config: next }))
+ else document.setIn([index, 'config'], document.createNode(next))
+ visit(document, { Map(_key, node) {
+ if (node.items.length !== 1 || typeof node.get('__jsExpr') !== 'string') return
+ const expression = new Scalar(node.get('__jsExpr'))
+ expression.tag = 'tag:yaml.org,2002:js'
+ return expression
+ } })
+ const profile = this.ownerContext.profileContext
+ const loaded = loadProfileDirectory('dsh', profile.dir, profile.installAnchor)
+ const patches = readProfilePatches('dsh', profile, { ...loaded, patches: yaml.load(String(document), { schema: entryListSchema }) as PatchOptions[] })
+ const effective = flatten(composeEntries([patches])).find(row => row.id === entry.options.id)
+ if (!isDeepStrictEqual(effective?.config ?? {}, next)) {
+ throw new Error(`Configuration for "${entry.options.id}" is overridden by a home patch or command-line overlay`)
+ }
+ await writeFileAtomic(path, String(document), { mode: 0o600 })
+ try {
+ await reconcileProfilePatches(this.ownerContext.root, patches, 'dsh', [entry.options.id])
+ } catch (error) {
+ await writeFileAtomic(path, before, { mode: 0o600 })
+ await reconcileProfilePatches(this.ownerContext.root, beforePatches, 'dsh')
+ throw error
+ }
+ })
+ }
+ const hmr = this.ownerContext.get('hmr')
+ await (hmr === undefined ? run() : hmr.runExclusive(run))
+ }
+}
+
+export default ConfigEditor
diff --git a/packages/settings/settings-file/tsconfig.json b/packages/boot/config-editor/tsconfig.json
similarity index 68%
rename from packages/settings/settings-file/tsconfig.json
rename to packages/boot/config-editor/tsconfig.json
index 25bb9d13e2..54ae6ebfe7 100644
--- a/packages/settings/settings-file/tsconfig.json
+++ b/packages/boot/config-editor/tsconfig.json
@@ -8,23 +8,23 @@
"src"
],
"references": [
- {
- "path": "../../../vendor/cosmokit"
- },
{
"path": "../../../vendor/cordis"
},
{
- "path": "../../../vendor/schemastery"
+ "path": "../../../vendor/loader"
+ },
+ {
+ "path": "../hmr"
+ },
+ {
+ "path": "../app-boot"
},
{
"path": "../../util/atomic-write"
},
{
- "path": "../../util/home-paths"
- },
- {
- "path": "../settings"
+ "path": "../../../vendor/include"
}
]
}
diff --git a/packages/bundle/base/cordis.patch.yml b/packages/bundle/base/cordis.patch.yml
index 079942f1d1..aeb6f29f69 100644
--- a/packages/bundle/base/cordis.patch.yml
+++ b/packages/bundle/base/cordis.patch.yml
@@ -91,11 +91,16 @@
- id: llm-retry
name: '@deepseek-ai/dsh-llm-retry'
- # User-settings document (`$DSH_HOME/settings.yaml`, hot-reloaded): a
- # `llm-deepseek:` or `llm-pi-ai:` section there overrides the adapter entries
- # below without a restart, and is what the web Models page writes.
+ # Live Config forms persist through the active profile patch.
+
+ # Live configuration forms persist through the launcher's profile.
+ - id: config-editor
+ name: '@deepseek-ai/dsh-config-editor'
+ disabled: !!js "!ctx.get('profileContext')"
+
- id: settings
- name: '@deepseek-ai/dsh-settings-file'
+ name: '@deepseek-ai/dsh-settings'
+ disabled: !!js "!ctx.get('profileContext')"
# Credential sources: inherited environment over the managed
# `$DSH_HOME/.credentials.yaml`, with project and user `.env` fallbacks.
diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json
index 8ef9704211..8df5d5b7ff 100644
--- a/packages/bundle/base/package.json
+++ b/packages/bundle/base/package.json
@@ -79,7 +79,6 @@
"@deepseek-ai/dsh-session-telemetry-otel": "workspace:^",
"@deepseek-ai/dsh-session-title": "workspace:^",
"@deepseek-ai/dsh-session-title-first-prompt-llm": "workspace:^",
- "@deepseek-ai/dsh-settings-file": "workspace:^",
"@deepseek-ai/dsh-skill": "workspace:^",
"@deepseek-ai/dsh-skill-badge": "workspace:^",
"@deepseek-ai/dsh-skill-filesystem": "workspace:^",
@@ -121,7 +120,9 @@
"@deepseek-ai/dsh-workflow-ptc": "workspace:^",
"@deepseek-ai/dsh-agent-instructions": "workspace:^",
"@deepseek-ai/dsh-ptc-runtime-node": "workspace:^",
- "@deepseek-ai/dsh-plugin-manager": "workspace:^"
+ "@deepseek-ai/dsh-plugin-manager": "workspace:^",
+ "@deepseek-ai/dsh-config-editor": "workspace:^",
+ "@deepseek-ai/dsh-settings": "workspace:^"
},
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^"
diff --git a/packages/bundle/base/tsconfig.json b/packages/bundle/base/tsconfig.json
index f1a449634c..2f01995a6b 100644
--- a/packages/bundle/base/tsconfig.json
+++ b/packages/bundle/base/tsconfig.json
@@ -10,6 +10,12 @@
"references": [
{
"path": "../../../vendor/cordis"
+ },
+ {
+ "path": "../../boot/config-editor"
+ },
+ {
+ "path": "../../settings/settings"
}
]
}
diff --git a/packages/client/locale/README.i18n.yaml b/packages/client/locale/README.i18n.yaml
index 4865a1b040..b2c2403dcd 100644
--- a/packages/client/locale/README.i18n.yaml
+++ b/packages/client/locale/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/locale/README.md
-README.md: 3d3771bf1cf61adf7048b5660cab6394af9cd9a0
-README.zh.md: d427c388de1b3b1e82635fbe39d3bb43b7515488
+README.md: 674ec03a8301757cdc63fc13cdad88d8e7303680
+README.zh.md: b526c4e1f4c931fcf2c6cf3923b87a8f93b7b85d
diff --git a/packages/client/locale/README.md b/packages/client/locale/README.md
index 3d3771bf1c..674ec03a83 100644
--- a/packages/client/locale/README.md
+++ b/packages/client/locale/README.md
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
## Summary
-Use `dsh-client-locale` to switch the web GUI between the shipped English and Chinese locales or languages added by client plugins. User selections take effect immediately; loopback pages persist them in `$DSH_HOME/settings.yaml`, while non-loopback pages keep them only for the current process. New browsers use the first supported language requested by the browser until an allowed stored preference arrives. Plugin authors add typed namespace dictionaries and translate through the public locale API; slot-rendered copy updates without a reload.
+Use `dsh-client-locale` to switch the web GUI between the shipped English and Chinese locales or languages added by client plugins. User selections take effect immediately; loopback pages persist them in `$DSH_HOME/cordis.patch.yml`, while non-loopback pages keep them only for the current process. New browsers use the first supported language requested by the browser until an allowed stored preference arrives. Plugin authors add typed namespace dictionaries and translate through the public locale API; slot-rendered copy updates without a reload.
## Table of Contents
diff --git a/packages/client/locale/README.zh.md b/packages/client/locale/README.zh.md
index d427c388de..b526c4e1f4 100644
--- a/packages/client/locale/README.zh.md
+++ b/packages/client/locale/README.zh.md
@@ -9,7 +9,7 @@ kind: "package-reference"
## 概述
-使用 `dsh-client-locale` 可在 web GUI 中切换内置的英文和中文 locale,或 client 插件添加的语言。用户选择会立即生效;loopback 页面把选择持久化到 `$DSH_HOME/settings.yaml`,非 loopback 页面则只为当前进程保留选择。全新浏览器会使用浏览器请求的第一个受支持语言,直到允许读取的已存储偏好到达。插件作者可添加类型化命名空间字典,并通过公开 locale API 翻译;经 slot 渲染的文案无需重新加载即可随语言切换更新。
+使用 `dsh-client-locale` 可在 web GUI 中切换内置的英文和中文 locale,或 client 插件添加的语言。用户选择会立即生效;loopback 页面把选择持久化到 `$DSH_HOME/cordis.patch.yml`,非 loopback 页面则只为当前进程保留选择。全新浏览器会使用浏览器请求的第一个受支持语言,直到允许读取的已存储偏好到达。插件作者可添加类型化命名空间字典,并通过公开 locale API 翻译;经 slot 渲染的文案无需重新加载即可随语言切换更新。
## 目录
diff --git a/packages/client/locale/src/client/index.ts b/packages/client/locale/src/client/index.ts
index cd1ef5cac1..969ce506d2 100644
--- a/packages/client/locale/src/client/index.ts
+++ b/packages/client/locale/src/client/index.ts
@@ -9,10 +9,10 @@ import type { LocalizedText } from '@deepseek-ai/dsh-package-manifest'
import {
type BoundActions, type LocaleDictOf, type LocaleNamespaceMap, type Translate, type TranslateNS,
} from '@deepseek-ai/dsh-client-ui-slots'
-// Type-only: the ctx.settingsScope Context merge and the settings slot types.
+// Type-only: the ctx.configForms Context merge and the settings slot types.
// Cross-plugin collaboration goes through the service, never a value import
// (client bundle purity gate).
-import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client'
+import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client'
// Type-only: pulls the SlotRegistry service merge (ctx.slots).
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
import {
@@ -168,7 +168,7 @@ export class LocaleRuntime {
private snapshot: LocaleSnapshot
private listeners = new Set<() => void>()
private readonly ctx: ClientContext
- private readonly host: SettingsScope