From dae6cad0658c91784df81071c28706d01ac96df2 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Mon, 10 Aug 2026 21:29:45 +0800 Subject: [PATCH] fix(web): show the plugin settings a user actually gets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three things the page got wrong. The web-search provider's defaults lived only at their use site, so the served section carried no value for them and the card fell back to a zero it invented — a number the schema itself rejects. Declaring them on the schema makes the settings service the one authority: `maxUses` now reads 5 because that is what the Host resolves, not because the page guessed. `baseURL` keeps its code-side default, which exists so `$DEEPSEEK_SEARCH_BASE_URL` can win. A field the Host serves no value for now renders empty rather than as zero. The cards were rows in a settings page of cards: name and description ran together on one line because the shared disclosure row lays them side by side. Each card now draws its own header, stacking the two, and the section follows the idiom the Agent Preset page established. --- .../created.expected.md | 4 +- .../damaged.expected.md | 4 +- .../section.expected.md | 4 +- .../models-settings/configured.expected.md | 4 +- .../models-settings/declared.expected.md | 4 +- .../models-settings/empty.expected.md | 4 +- .../models.expected.md | 4 +- .../plugin-config/section.expected.md | 18 ++-- .../settings-chrome/dialog.expected.md | 4 +- packages/client/ui-plugin-config/package.json | 5 +- .../src/client/AgentLoopCard.tsx | 10 +-- .../ui-plugin-config/src/client/BashCard.tsx | 10 +-- .../src/client/PluginCard.module.css | 79 ++++++++++++++---- .../src/client/PluginCard.tsx | 83 +++++++++++-------- .../src/client/PluginConfigSection.module.css | 24 +++--- .../src/client/WebSearchCard.tsx | 10 +-- .../src/client/agent-loop-store.ts | 4 +- .../ui-plugin-config/src/client/bash-store.ts | 8 +- .../src/client/fields.module.css | 47 +++++++---- .../ui-plugin-config/src/client/fields.tsx | 11 ++- .../ui-plugin-config/src/client/locales.ts | 10 +-- .../src/client/web-search-store.ts | 4 +- .../ui-plugin-config/tests/apply.spec.ts | 2 +- .../ui-plugin-config/tests/fields.spec.tsx | 16 ++++ .../ui-settings/src/client/SettingsRoot.tsx | 4 +- packages/web/web-search-deepseek/src/index.ts | 11 ++- pnpm-lock.yaml | 4 + 27 files changed, 241 insertions(+), 151 deletions(-) diff --git a/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md b/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md index aece772fe4..b26cba28f4 100644 --- a/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md +++ b/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md @@ -10,9 +10,9 @@ - button "Agent 预设": - img - text: Agent 预设 - - button "插件": + - button "插件配置": - img - - text: 插件 + - text: 插件配置 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md b/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md index 7620e679c8..14a6337736 100644 --- a/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md +++ b/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md @@ -10,9 +10,9 @@ - button "Agent 预设": - img - text: Agent 预设 - - button "插件": + - button "插件配置": - img - - text: 插件 + - text: 插件配置 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md b/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md index dade3d7b84..7c35b40786 100644 --- a/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md +++ b/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md @@ -10,9 +10,9 @@ - button "Agent 预设": - img - text: Agent 预设 - - button "插件": + - button "插件配置": - img - - text: 插件 + - text: 插件配置 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/models-settings/configured.expected.md b/apps/web/tests/snapshots/models-settings/configured.expected.md index 3b57d5ac6f..e4fb6e13e8 100644 --- a/apps/web/tests/snapshots/models-settings/configured.expected.md +++ b/apps/web/tests/snapshots/models-settings/configured.expected.md @@ -10,9 +10,9 @@ - button "Agent 预设": - img - text: Agent 预设 - - button "插件": + - button "插件配置": - img - - text: 插件 + - text: 插件配置 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/models-settings/declared.expected.md b/apps/web/tests/snapshots/models-settings/declared.expected.md index dd3f03776f..b126a5025b 100644 --- a/apps/web/tests/snapshots/models-settings/declared.expected.md +++ b/apps/web/tests/snapshots/models-settings/declared.expected.md @@ -10,9 +10,9 @@ - button "Agent 预设": - img - text: Agent 预设 - - button "插件": + - button "插件配置": - img - - text: 插件 + - text: 插件配置 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/models-settings/empty.expected.md b/apps/web/tests/snapshots/models-settings/empty.expected.md index 16cc93581b..5a1dba54ee 100644 --- a/apps/web/tests/snapshots/models-settings/empty.expected.md +++ b/apps/web/tests/snapshots/models-settings/empty.expected.md @@ -10,9 +10,9 @@ - button "Agent 预设": - img - text: Agent 预设 - - button "插件": + - button "插件配置": - img - - text: 插件 + - text: 插件配置 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/onboarding-deepseek-config/models.expected.md b/apps/web/tests/snapshots/onboarding-deepseek-config/models.expected.md index b5f25aaaa6..2624f4db70 100644 --- a/apps/web/tests/snapshots/onboarding-deepseek-config/models.expected.md +++ b/apps/web/tests/snapshots/onboarding-deepseek-config/models.expected.md @@ -10,9 +10,9 @@ - button "Agent 预设": - img - text: Agent 预设 - - button "插件": + - button "插件配置": - img - - text: 插件 + - text: 插件配置 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/plugin-config/section.expected.md b/apps/web/tests/snapshots/plugin-config/section.expected.md index 54ef09851f..7d10d05cd1 100644 --- a/apps/web/tests/snapshots/plugin-config/section.expected.md +++ b/apps/web/tests/snapshots/plugin-config/section.expected.md @@ -10,25 +10,25 @@ - button "Agent 预设": - img - text: Agent 预设 - - button "插件": + - button "插件配置": - img - - text: 插件 + - text: 插件配置 - button "打开配置文件" - button "关闭": - img - text: 关闭 - heading "插件配置" [level=2] - - paragraph: 本部署所组装插件自己拥有的设置。你在这里设的值会覆盖组装默认值,并在下一次使用时生效。 + - paragraph: 配置本部署已安装的插件。 - list: - listitem: - - button "终端 限制 agent 运行的每一条命令。": - - img + - 'button "展开设置: 终端"': - text: 终端 限制 agent 运行的每一条命令。 - - listitem: - - button "Agent 循环 Agent 如何派发工具调用。": - img + - listitem: + - 'button "展开设置: Agent 循环"': - text: Agent 循环 Agent 如何派发工具调用。 - - listitem: - - button "网页搜索 DeepSeek 搜索提供方。": - img + - listitem: + - 'button "展开设置: 网页搜索"': - text: 网页搜索 DeepSeek 搜索提供方。 + - img diff --git a/apps/web/tests/snapshots/settings-chrome/dialog.expected.md b/apps/web/tests/snapshots/settings-chrome/dialog.expected.md index 3ac0245395..914293aee3 100644 --- a/apps/web/tests/snapshots/settings-chrome/dialog.expected.md +++ b/apps/web/tests/snapshots/settings-chrome/dialog.expected.md @@ -10,9 +10,9 @@ - button "Agent 预设": - img - text: Agent 预设 - - button "插件": + - button "插件配置": - img - - text: 插件 + - text: 插件配置 - button "打开配置文件" - button "关闭": - img diff --git a/packages/client/ui-plugin-config/package.json b/packages/client/ui-plugin-config/package.json index f3a9c04e62..61cec64110 100644 --- a/packages/client/ui-plugin-config/package.json +++ b/packages/client/ui-plugin-config/package.json @@ -67,5 +67,8 @@ "lib/invariant.js", "lib/client.js", "lib/types/**/*.d.ts" - ] + ], + "dependencies": { + "clsx": "^2.0.0" + } } diff --git a/packages/client/ui-plugin-config/src/client/AgentLoopCard.tsx b/packages/client/ui-plugin-config/src/client/AgentLoopCard.tsx index 99790467dc..1e47453eff 100644 --- a/packages/client/ui-plugin-config/src/client/AgentLoopCard.tsx +++ b/packages/client/ui-plugin-config/src/client/AgentLoopCard.tsx @@ -1,6 +1,5 @@ /** The agent-loop plugin's card: how many tool calls may run at once. */ -import { useState } from 'react' import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client' import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import { NumberField } from './fields.tsx' @@ -34,17 +33,14 @@ export type AgentLoopCardProps = export function AgentLoopCard(props: AgentLoopCardProps) { const { t } = props const state = props.useAgentLoopCard(snapshot => snapshot) - const [open, setOpen] = useState(false) const disabled = !state.writable return ( { setOpen(!open) }} readOnly={disabled} - readOnlyLabel={t('readOnly')} > snapshot) - const [open, setOpen] = useState(false) const disabled = !state.writable return ( { setOpen(!open) }} readOnly={disabled} - readOnlyLabel={t('readOnly')} > string + /** Locale key of the plugin's name. */ + titleKey: PluginConfigKey + /** Locale key of the line describing what this plugin's settings govern. */ + descriptionKey: PluginConfigKey /** False while the namespace is not served to this client. */ available: boolean - /** Whether the card body is showing. */ - open: boolean - /** Toggle the card body. */ - onToggle: () => void - /** Copy shown when the settings document refuses writes. */ - readOnlyLabel?: string | undefined - /** True when the Host document is read-only. */ + /** True when the Host document is read-only, which disables the fields. */ readOnly: boolean /** The plugin's controls. */ children: ReactNode @@ -31,31 +37,36 @@ export interface PluginCardProps { /** * Render one plugin card. - * @param props - card chrome, disclosure state, and the plugin's controls. + * @param props - the plugin's copy keys, its availability, and its controls. * @returns the card, or nothing when the namespace is unavailable. */ export function PluginCard(props: PluginCardProps) { + const [open, setOpen] = useState(false) if (!props.available) return null + const title = props.t(props.titleKey) return ( -
  • - {props.description}} +
  • + + {open + ? ( +
    + {props.readOnly ?

    {props.t('readOnly')}

    : null} + {props.children} +
    + ) + : null}
  • ) } diff --git a/packages/client/ui-plugin-config/src/client/PluginConfigSection.module.css b/packages/client/ui-plugin-config/src/client/PluginConfigSection.module.css index 6f2af513e9..45c9a78f00 100644 --- a/packages/client/ui-plugin-config/src/client/PluginConfigSection.module.css +++ b/packages/client/ui-plugin-config/src/client/PluginConfigSection.module.css @@ -3,34 +3,34 @@ .section { display: flex; flex-direction: column; + gap: 12px; + max-width: 720px; + color: var(--dsw-alias-label-primary); } .heading { margin: 0; - font-size: 16px; - font-weight: 500; - line-height: 24px; - color: var(--dsw-alias-label-primary); + font-size: 18px; + font-weight: 600; } .intro { - margin: 8px 0 16px; - font-size: 12px; - font-weight: 400; - line-height: 18px; + margin: 0; + font-size: 13px; color: var(--dsw-alias-label-tertiary); } .cards { + list-style: none; margin: 0; padding: 0; - list-style: none; + display: flex; + flex-direction: column; + gap: 10px; } .empty { margin: 0; - padding: 16px 0; - font-size: 14px; - line-height: 22px; + font-size: 13px; color: var(--dsw-alias-label-tertiary); } diff --git a/packages/client/ui-plugin-config/src/client/WebSearchCard.tsx b/packages/client/ui-plugin-config/src/client/WebSearchCard.tsx index 72c11fa545..702a7a6627 100644 --- a/packages/client/ui-plugin-config/src/client/WebSearchCard.tsx +++ b/packages/client/ui-plugin-config/src/client/WebSearchCard.tsx @@ -4,7 +4,6 @@ * the settings section, so the literal never rides a response. */ -import { useState } from 'react' import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client' import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' import { NumberField, SecretField, TextField } from './fields.tsx' @@ -44,17 +43,14 @@ export type WebSearchCardProps = export function WebSearchCard(props: WebSearchCardProps) { const { t } = props const state = props.useWebSearchCard(snapshot => snapshot) - const [open, setOpen] = useState(false) const disabled = !state.writable return ( { setOpen(!open) }} readOnly={disabled} - readOnlyLabel={t('readOnly')} > + maxParallelToolCalls: CardField } /** The registration-side face the agent-loop card's slot entry injects. */ @@ -42,7 +42,7 @@ export class AgentLoopCardController extends CardController) { super(scope, snapshot => ({ ...shellOf(snapshot), - maxParallelToolCalls: fieldOf(snapshot, 'maxParallelToolCalls', 0), + maxParallelToolCalls: fieldOf(snapshot, 'maxParallelToolCalls', undefined), })) } diff --git a/packages/client/ui-plugin-config/src/client/bash-store.ts b/packages/client/ui-plugin-config/src/client/bash-store.ts index 91114669c1..837a6fab04 100644 --- a/packages/client/ui-plugin-config/src/client/bash-store.ts +++ b/packages/client/ui-plugin-config/src/client/bash-store.ts @@ -21,9 +21,9 @@ export interface BashSettings { /** What the shell card renders. */ export interface BashCardState extends CardShell { /** Command timeout in milliseconds. */ - timeoutMs: CardField + timeoutMs: CardField /** Per-stream output cap in bytes. */ - maxOutputBytes: CardField + maxOutputBytes: CardField } /** The registration-side face the shell card's slot entry injects. */ @@ -50,8 +50,8 @@ export class BashCardController extends CardController void }) { @@ -124,13 +127,13 @@ export function NumberField(props: FieldProps & { { const parsed = Number(draft) if (draft.trim() === '' || !Number.isFinite(parsed)) { - restore(String(props.value)) + restore(props.value === undefined ? '' : String(props.value)) return } if (parsed === props.value) return diff --git a/packages/client/ui-plugin-config/src/client/locales.ts b/packages/client/ui-plugin-config/src/client/locales.ts index 6a3804236c..7babcc1bdd 100644 --- a/packages/client/ui-plugin-config/src/client/locales.ts +++ b/packages/client/ui-plugin-config/src/client/locales.ts @@ -13,11 +13,9 @@ export type PluginConfigKey = /** English copy. */ export const en: Record = { - nav: 'Plugins', + nav: 'Plugin config', title: 'Plugin configuration', - intro: - 'Settings owned by the plugins this deployment composes. A value you set here layers over the ' - + 'composition default and applies to the next use.', + intro: 'Configure the plugins this deployment installed.', empty: 'This deployment exposes no plugin settings.', overridden: 'Overridden', reset: 'Reset to default', @@ -48,9 +46,9 @@ export const en: Record = { /** Simplified Chinese copy. */ export const zh: Record = { - nav: '插件', + nav: '插件配置', title: '插件配置', - intro: '本部署所组装插件自己拥有的设置。你在这里设的值会覆盖组装默认值,并在下一次使用时生效。', + intro: '配置本部署已安装的插件。', empty: '本部署没有开放任何插件设置。', overridden: '已覆盖', reset: '恢复默认', diff --git a/packages/client/ui-plugin-config/src/client/web-search-store.ts b/packages/client/ui-plugin-config/src/client/web-search-store.ts index c0242eed8b..2def74cb79 100644 --- a/packages/client/ui-plugin-config/src/client/web-search-store.ts +++ b/packages/client/ui-plugin-config/src/client/web-search-store.ts @@ -36,7 +36,7 @@ export interface WebSearchCardState extends CardShell { /** Provider endpoint. */ baseURL: CardField /** Searches allowed per request. */ - maxUses: CardField + maxUses: CardField /** Credential reference the key is written under. */ apiKeyRef: string /** Whether the Host reports a credential configured for that reference. */ @@ -78,7 +78,7 @@ export class WebSearchCardController extends CardController ({ ...shellOf(snapshot), baseURL: fieldOf(snapshot, 'baseURL', ''), - maxUses: fieldOf(snapshot, 'maxUses', 0), + maxUses: fieldOf(snapshot, 'maxUses', undefined), apiKeyRef: refOf(snapshot), apiKeyConfigured: credential.configured, })) diff --git a/packages/client/ui-plugin-config/tests/apply.spec.ts b/packages/client/ui-plugin-config/tests/apply.spec.ts index 86bb21606a..233f0cd337 100644 --- a/packages/client/ui-plugin-config/tests/apply.spec.ts +++ b/packages/client/ui-plugin-config/tests/apply.spec.ts @@ -48,7 +48,7 @@ describe('ui-plugin-config apply', () => { const section = slots.entries('settings.section')[0]! expect(section.options).toMatchObject({ id: 'plugins', order: 30 }) // The nav label is a locale-following thunk; owners resolve it at read time. - expect(resolveSlotLabel(section.options.label)).toBe('插件') + expect(resolveSlotLabel(section.options.label)).toBe('插件配置') expect(slots.spec('settings.plugin.item')).toMatchObject({ kind: 'list', scope: 'root' }) }) diff --git a/packages/client/ui-plugin-config/tests/fields.spec.tsx b/packages/client/ui-plugin-config/tests/fields.spec.tsx index d444ae14a8..0248fee10d 100644 --- a/packages/client/ui-plugin-config/tests/fields.spec.tsx +++ b/packages/client/ui-plugin-config/tests/fields.spec.tsx @@ -116,6 +116,22 @@ describe('NumberField', () => { expect(onCommit).not.toHaveBeenCalled() }) + it('renders an absent value as empty rather than as a number nobody chose', () => { + const onCommit = vi.fn() + render( + , + ) + const input = screen.getByLabelText('Command timeout') + expect(input).toHaveProperty('value', '') + + // A draft typed and then cleared restores the same emptiness, not a zero. + fireEvent.change(input, { target: { value: 'abc' } }) + fireEvent.blur(input) + + expect(input).toHaveProperty('value', '') + expect(onCommit).not.toHaveBeenCalled() + }) + it('suppresses every interaction while disabled', () => { const onCommit = vi.fn() const onReset = vi.fn() diff --git a/packages/client/ui-settings/src/client/SettingsRoot.tsx b/packages/client/ui-settings/src/client/SettingsRoot.tsx index 54e0e0dbb7..23f5421ff3 100644 --- a/packages/client/ui-settings/src/client/SettingsRoot.tsx +++ b/packages/client/ui-settings/src/client/SettingsRoot.tsx @@ -14,7 +14,8 @@ import { useCallback, useEffect, useId, useRef, useState } from 'react' import clsx from 'clsx' import { - IconCloseOutline16, IconDataOutline16, IconSettingsOutline16, IconThinkOutline16, + IconCloseOutline16, IconDataOutline16, IconPersonalizationOutline16, + IconSettingsOutline16, IconThinkOutline16, } from '@deepseek-ai/dsh-client-ui-primitives' import type { SettingsRootComponentProps, SettingsSectionRow } from './contract/slots.ts' import css from './SettingsRoot.module.css' @@ -23,6 +24,7 @@ import css from './SettingsRoot.module.css' function navIcon(id: string) { if (id === 'models') return if (id === 'agent-presets') return + if (id === 'plugins') return return } diff --git a/packages/web/web-search-deepseek/src/index.ts b/packages/web/web-search-deepseek/src/index.ts index 50d9c69e22..bb3a6efe0a 100644 --- a/packages/web/web-search-deepseek/src/index.ts +++ b/packages/web/web-search-deepseek/src/index.ts @@ -63,11 +63,14 @@ export interface Config { export const Config: z = z.object({ apiKey: z.string().role('secret'), apiKeyEnv: z.string().role('credential-ref').default(DEFAULT_API_KEY_ENV), + // Declared here rather than only at the use site: a configuration surface + // renders the resolved section, so a default the schema does not carry reads + // there as no value at all. baseURL: z.string(), - model: z.string(), - apiVersion: z.string(), - maxTokens: z.number().step(1).min(1), - maxUses: z.number().step(1).min(1), + model: z.string().default(DEEPSEEK_DEFAULT_MODEL), + apiVersion: z.string().default(DEEPSEEK_DEFAULT_API_VERSION), + maxTokens: z.number().step(1).min(1).default(DEEPSEEK_DEFAULT_MAX_TOKENS), + maxUses: z.number().step(1).min(1).default(DEEPSEEK_DEFAULT_MAX_USES), }) /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c1950a855..50b497608c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2291,6 +2291,10 @@ importers: version: 18.3.1 packages/client/ui-plugin-config: + dependencies: + clsx: + specifier: ^2.0.0 + version: 2.1.1 devDependencies: '@deepseek-ai/dsh-client-connection': specifier: workspace:^