diff --git a/packages/client/ui-agent-preset/README.i18n.yaml b/packages/client/ui-agent-preset/README.i18n.yaml index 40e443b224..97e553be99 100644 --- a/packages/client/ui-agent-preset/README.i18n.yaml +++ b/packages/client/ui-agent-preset/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-agent-preset/README.md -README.md: d775daf1e91c6eb9ebca69c7d0484c0029e93cfc -README.zh.md: b3fb3e64324dcb78465843ba2ba58f5a3efa3d08 +README.md: be159855010c1a0af7d3f712d9551000826d4820 +README.zh.md: 73c07751f4e5cf1cea43d2723027162f09da10f6 diff --git a/packages/client/ui-agent-preset/README.md b/packages/client/ui-agent-preset/README.md index 9425b50af9..45e4ceabbb 100644 --- a/packages/client/ui-agent-preset/README.md +++ b/packages/client/ui-agent-preset/README.md @@ -24,7 +24,7 @@ The row re-reads on `settings/changed` for its own namespace and on `connection/ ## The management section -A third surface, its own settings page: the roster as rows, and one composition open in a YAML editor at a time. +A third surface, its own settings page (`settings.section` id `agent-presets`, ordered after Models — choosing a model is routine, composing an agent is the deployment-shaping act behind it): the roster as rows, and one composition open in a YAML editor at a time. A shipped preset opens read-only. It is the known-good composition a local one is written against, so reading it is the point and overwriting it is not — the deployment's copy is what a broken local preset is compared against. Authoring therefore starts by duplicating: **New preset** copies the current default, and **Duplicate** copies any row, because a copy always lands in the local root regardless of where the text came from. @@ -32,6 +32,8 @@ An id becomes a directory name, so the editor mirrors the host's own containment Deleting removes the file. Sessions already composed from it keep running — a composition is mounted once at session creation and nothing re-reads the file. +Setting the default writes the `agent-presets` settings namespace, which the host exposes to configuration clients ([`dsh-apiproxy`](../../host/apiproxy/README.md) keeps an explicit allowlist — a namespace outside it makes a picker move and then silently forget). + `agentPreset.read`, `write`, `remove`, and `select` are loopback-pinned ([`dsh-client-connection`](../connection/README.md)): a composition names the plugins a session runs, so reading one is reconnaissance and writing one is arbitrary capability. `agentPreset.list` is not — it carries ids and trust, and a LAN client's picker needs it. ## When the surfaces are absent diff --git a/packages/client/ui-agent-preset/README.zh.md b/packages/client/ui-agent-preset/README.zh.md index c5be1ea017..3552e92675 100644 --- a/packages/client/ui-agent-preset/README.zh.md +++ b/packages/client/ui-agent-preset/README.zh.md @@ -24,7 +24,7 @@ agent preset 的各个表层:General 设置中的一行,用于选择新建 ## 管理分区 -第三个表层,独立的设置页:名单以行呈现,同一时刻有一份组装在 YAML 编辑器中打开。 +第三个表层,独立的设置页(`settings.section`,id 为 `agent-presets`,排在「模型」之后——选模型是日常操作,而组装 agent 是它背后那件塑造部署形态的事):名单以行呈现,同一时刻有一份组装在 YAML 编辑器中打开。 随部署提供的 preset 以只读方式打开。它是本地 preset 据以编写的已知良好组装,因此能读到它正是意义所在,而覆写它则不是——部署自带的那一份正是用来对照有问题的本地 preset 的。因此创作从复制开始:**新建 preset** 复制当前默认值,**复制**则复制任意一行;无论文本来自何处,副本总是落在本地根目录,所以副本总是可写的。 @@ -32,6 +32,8 @@ id 会成为目录名,因此编辑器复刻宿主自身的约束规则(`[a-z 删除会移除该文件。已据其组装的会话继续运行——组装在会话创建时挂载一次,此后没有任何东西会重新读取该文件。 +设置默认值写入的是 `agent-presets` settings 命名空间,宿主需将其暴露给配置客户端([`dsh-apiproxy`](../../host/apiproxy/README.md) 维护一份显式白名单——不在其中的命名空间会让选择器动一下然后悄悄忘记)。 + `agentPreset.read`、`write`、`remove` 与 `select` 被固定在环回地址(见 [`dsh-client-connection`](../connection/README.md)):组装指明了一个会话所运行的插件,因此读取它是侦察,写入它是任意能力。`agentPreset.list` 不在其中——它只携带 id 与信任级别,而局域网客户端的选择器需要它。 ## 何时不显示这些表层 diff --git a/packages/client/ui-agent-preset/src/client/locales.ts b/packages/client/ui-agent-preset/src/client/locales.ts index fc97b4d109..95d9c5bb3b 100644 --- a/packages/client/ui-agent-preset/src/client/locales.ts +++ b/packages/client/ui-agent-preset/src/client/locales.ts @@ -59,7 +59,7 @@ export const zh: Record = { userTrust: '本地', seatHint: '本会话的 agent preset —— 发送第一条消息前可切换', lockedHint: '会话开始后,其 agent preset 即固定', - nav: 'Agent preset', + nav: '智能体', sectionIntro: 'preset 即一个会话的 agent 所运行的插件组装 —— 它的工具、提示词与能力。内置 preset 只读;复制一份即可改成自己的。', builtIn: '内置', defaultBadge: '默认', diff --git a/packages/client/ui-agent-preset/tests/apply.spec.ts b/packages/client/ui-agent-preset/tests/apply.spec.ts index 207598f471..40da44c792 100644 --- a/packages/client/ui-agent-preset/tests/apply.spec.ts +++ b/packages/client/ui-agent-preset/tests/apply.spec.ts @@ -91,7 +91,7 @@ describe('ui-agent-preset apply', () => { expect(section.component).toBe(AgentPresetSection) expect(section.options).toMatchObject({ id: 'agent-presets', order: 20 }) // The nav label is a locale-following thunk; owners resolve it at read time. - expect(resolveSlotLabel(section.options.label)).toBe('Agent preset') + expect(resolveSlotLabel(section.options.label)).toBe('智能体') }) it('registers into a declaration that arrives after apply', async () => { diff --git a/packages/client/ui-settings/src/client/SettingsRoot.tsx b/packages/client/ui-settings/src/client/SettingsRoot.tsx index 45055753ac..9e48c7d4b0 100644 --- a/packages/client/ui-settings/src/client/SettingsRoot.tsx +++ b/packages/client/ui-settings/src/client/SettingsRoot.tsx @@ -12,13 +12,16 @@ import { useCallback, useEffect, useId, useRef, useState } from 'react' import { createPortal } from 'react-dom' import clsx from 'clsx' -import { IconCloseOutline16, IconDataOutline16, IconSettingsOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' +import { + IconCloseOutline16, IconDataOutline16, IconSettingsOutline16, IconThinkOutline16, +} from '@deepseek-ai/dsh-client-ui-primitives' import type { SettingsRootComponentProps, SettingsSectionRow } from './contract/slots.ts' import css from './SettingsRoot.module.css' /** Nav glyph by section id; unknown ids fall back to the settings gear. */ function navIcon(id: string) { if (id === 'models') return + if (id === 'agent-presets') return return } diff --git a/packages/client/ui-settings/tests/settings-root.spec.tsx b/packages/client/ui-settings/tests/settings-root.spec.tsx index 900c66d381..732ca4ba81 100644 --- a/packages/client/ui-settings/tests/settings-root.spec.tsx +++ b/packages/client/ui-settings/tests/settings-root.spec.tsx @@ -24,6 +24,7 @@ function mount({ rows = [ { id: 'general', order: 0, label: 'General' }, { id: 'models', order: 10, label: 'Models' }, + { id: 'agent-presets', order: 20, label: 'Agent presets' }, ], steps = [ { id: 'welcome', order: -100 }, diff --git a/packages/host/apiproxy/src/api-proxy.ts b/packages/host/apiproxy/src/api-proxy.ts index 353d5a7af8..2285852da2 100644 --- a/packages/host/apiproxy/src/api-proxy.ts +++ b/packages/host/apiproxy/src/api-proxy.ts @@ -26,7 +26,8 @@ import { // Type-only: brings the `ctx.tools` Context merge into this program (viewFor reads presenters). import { InvalidCompositionError, InvalidPresetIdError, PresetMountError, - PresetNotWritableError, resolveSessionPreset, UnknownPresetError, + PresetNotWritableError, resolveSessionPreset, + SETTINGS_NAMESPACE as AGENT_PRESET_SETTINGS_NAMESPACE, UnknownPresetError, } from '@deepseek-ai/dsh-agent-presets' import type {} from '@deepseek-ai/dsh-tools' import type { @@ -89,8 +90,15 @@ const COLD_SUMMARY_BATCH_SIZE = 16 /** Conversation message event types (the pagination counting unit). */ const MESSAGE_TYPES = new Set(['user/message', 'assistant/message']) -/** Product settings intentionally exposed beside model-provider namespaces. */ -const PRODUCT_SETTINGS_NAMESPACES = new Set(['ui-onboarding']) +/** + * Product settings intentionally exposed beside model-provider namespaces. + * + * The agent-preset namespace carries one field — which preset a session with + * no explicit choice is composed from — and both browser surfaces that offer + * that choice write it through `settings.update`, so it has to cross the + * configuration boundary or the pickers silently fail to persist. + */ +const PRODUCT_SETTINGS_NAMESPACES = new Set(['ui-onboarding', AGENT_PRESET_SETTINGS_NAMESPACE]) /** Read live abort state across awaits without treating it as synchronously immutable. */ function isAborted(signal: AbortSignal): boolean { diff --git a/packages/host/apiproxy/tests/api-proxy-config.spec.ts b/packages/host/apiproxy/tests/api-proxy-config.spec.ts index 54235c0218..d606c12c4a 100644 --- a/packages/host/apiproxy/tests/api-proxy-config.spec.ts +++ b/packages/host/apiproxy/tests/api-proxy-config.spec.ts @@ -355,6 +355,21 @@ describe('settings domain', () => { expect(frames).toEqual([{ type: 'host/settings-changed', ns: 'ui-onboarding' }]) }) + it('serves the agent-preset namespace, so a browser preset picker can persist its choice', async () => { + const ctx = await harness() + ctx.settings.register(settingsNamespace('agent-presets'), z.object({ default: z.string() })) + const api = createApiProxy(ctx, DEFAULTS) + + expectOk(await api.settings.update(request({ ns: 'agent-presets', patch: { default: 'core-web' } }))) + + // Both browser surfaces that offer the choice — the General row and the + // management section — write the default through `settings.update`, so a + // namespace outside this boundary makes the picker move and then silently + // forget, which is worse than refusing the control. + expect(ctx.settings.describe().find(view => String(view.ns) === 'agent-presets')?.value) + .toEqual({ default: 'core-web' }) + }) + it('refuses even a model-provider namespace once its directory entry is gone', async () => { const ctx = await harness({ configurableProviders: false }) ctx.settings.register(NS, AdapterConfig)