From 0b2f476071ebeddd8836884e63af299152d7064b Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Wed, 26 Aug 2026 11:45:15 +0800 Subject: [PATCH] fix(ui-settings-plugins): align Subagent configuration card --- .../plugin-config/section.expected.md | 4 +- apps/web/tests/plugin-config.e2e.ts | 13 ++-- .../SubagentModelSelectionCard.module.css | 25 +++++-- .../src/client/SubagentModelSelectionCard.tsx | 74 ++++++++++++++----- .../ui-settings-plugins/src/client/locales.ts | 53 ++++++------- ...ubagent-model-selection-card-controller.ts | 26 ++----- .../tests/section.client.spec.tsx | 11 ++- .../tests/stores.client.spec.ts | 5 +- 8 files changed, 124 insertions(+), 87 deletions(-) diff --git a/apps/web/tests/expected/plugin-config/section.expected.md b/apps/web/tests/expected/plugin-config/section.expected.md index 39cfa33d85..1a95e7032d 100644 --- a/apps/web/tests/expected/plugin-config/section.expected.md +++ b/apps/web/tests/expected/plugin-config/section.expected.md @@ -25,8 +25,8 @@ - tabpanel "插件配置": - list: - listitem: - - 'button "展开设置: Subagent 自选模型"': - - text: Subagent 自选模型 选择新会话允许为 subagent 自选的模型。运行中的会话不会改变。 + - 'button "展开设置: Subagent"': + - text: Subagent 控制 Agent 为 Subagent 选择模型的权限。 - img - listitem: - 'button "展开设置: 终端"': diff --git a/apps/web/tests/plugin-config.e2e.ts b/apps/web/tests/plugin-config.e2e.ts index edf3b1e98f..e3b34776bb 100644 --- a/apps/web/tests/plugin-config.e2e.ts +++ b/apps/web/tests/plugin-config.e2e.ts @@ -77,8 +77,8 @@ describe('web e2e: plugin configuration section', () => { // Every card the shipped web composition exposes: subagent selection, the // shell executor, the agent loop, and the DeepSeek search provider. - await dialog.getByText('Subagent 自选模型', { exact: true }).waitFor({ timeout: 10_000 }) - expect(await dialog.getByRole('button', { name: '展开设置: Subagent 自选模型' }).count()).toBe(1) + await dialog.getByText('Subagent', { exact: true }).waitFor({ timeout: 10_000 }) + expect(await dialog.getByRole('button', { name: '展开设置: Subagent' }).count()).toBe(1) await dialog.getByText('终端', { exact: true }).waitFor({ timeout: 10_000 }) expect(await dialog.getByText('Agent 循环', { exact: true }).count()).toBe(1) expect(await dialog.getByText('网页搜索', { exact: true }).count()).toBe(1) @@ -93,11 +93,11 @@ describe('web e2e: plugin configuration section', () => { it('persists selected adapter routes as the subagent model allowlist', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-plugin-config-subagent-model-selection')) const dialog = await openPlugins() - await dialog.getByText('Subagent 自选模型', { exact: true }).click() - const toggle = dialog.getByRole('switch', { name: '允许 subagent 自选模型' }) + await dialog.getByText('Subagent', { exact: true }).click() + const toggle = dialog.getByRole('switch', { name: '允许 Agent 为 Subagent 选择模型' }) await toggle.click() - const models = dialog.getByRole('group', { name: '允许的模型' }) + const models = dialog.getByRole('group', { name: 'Agent 可选择的模型' }) await models.waitFor({ timeout: 10_000 }) const firstModel = models.getByRole('checkbox').first() await firstModel.check() @@ -110,7 +110,8 @@ describe('web e2e: plugin configuration section', () => { expect(await settingsDocument()).toContain('allowedModels:') expect(await settingsDocument()).toContain('provider:') expect(await settingsDocument()).toContain('model:') - expect(await dialog.getByRole('status').textContent()).toBe('已保存,新会话将使用此设置。') + await expect.poll(() => dialog.getByRole('button', { name: '保存', exact: true }).isDisabled()).toBe(true) + expect(await dialog.getByText('未保存', { exact: true }).count()).toBe(0) await toggle.click() await dialog.getByRole('button', { name: '保存', exact: true }).click() diff --git a/packages/client/ui-settings-plugins/src/client/SubagentModelSelectionCard.module.css b/packages/client/ui-settings-plugins/src/client/SubagentModelSelectionCard.module.css index 075564224a..c71952a0bd 100644 --- a/packages/client/ui-settings-plugins/src/client/SubagentModelSelectionCard.module.css +++ b/packages/client/ui-settings-plugins/src/client/SubagentModelSelectionCard.module.css @@ -54,8 +54,7 @@ .hint, .notice, -.invalid, -.status { +.invalid { margin: 0; font-size: 12px; line-height: 1.5; @@ -70,10 +69,6 @@ color: var(--dsw-alias-label-error); } -.status { - color: var(--dsw-alias-state-success-primary); -} - .catalogError { display: flex; align-items: center; @@ -109,6 +104,24 @@ color: var(--dsw-alias-label-secondary); } +.modelGroup { + display: grid; + gap: 6px; +} + +.modelGroup + .modelGroup { + margin-top: 4px; + padding-top: 10px; + border-top: 1px solid var(--dsw-alias-border-l3); +} + +.providerName { + padding: 0 6px; + font-size: 11px; + font-weight: 500; + color: var(--dsw-alias-label-tertiary); +} + .model { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; diff --git a/packages/client/ui-settings-plugins/src/client/SubagentModelSelectionCard.tsx b/packages/client/ui-settings-plugins/src/client/SubagentModelSelectionCard.tsx index 0a8ce094fd..962dfdb41c 100644 --- a/packages/client/ui-settings-plugins/src/client/SubagentModelSelectionCard.tsx +++ b/packages/client/ui-settings-plugins/src/client/SubagentModelSelectionCard.tsx @@ -2,7 +2,10 @@ import clsx from 'clsx' import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' -import type { SubagentModelSelectionCardFace } from './subagent-model-selection-card-controller.ts' +import type { + SubagentModelCandidate, + SubagentModelSelectionCardFace, +} from './subagent-model-selection-card-controller.ts' import type {} from './slot-contract.ts' import { PluginCard } from './PluginCard.tsx' import css from './SubagentModelSelectionCard.module.css' @@ -21,6 +24,43 @@ export type SubagentModelSelectionCardProps = export function SubagentModelSelectionCard(props: SubagentModelSelectionCardProps) { const { t } = props const state = props.useSubagentModelSelectionCard(snapshot => snapshot) + const availableGroups = new Map() + const unavailable: SubagentModelCandidate[] = [] + for (const candidate of state.candidates) { + if (!candidate.available) { + unavailable.push(candidate) + continue + } + const group = availableGroups.get(candidate.provider) + if (group === undefined) { + availableGroups.set(candidate.provider, { + providerName: candidate.providerName, + candidates: [candidate], + }) + } else { + group.candidates.push(candidate) + } + } + const renderCandidate = (candidate: SubagentModelCandidate) => ( + + ) return ( ) : null} - {state.catalogFailures.length > 0 + {state.catalogPartial ?

{t('subagentModelSelectionPartial')}

: null} {state.candidates.length > 0 ? (
{t('subagentModelSelectionAllowed')} - {state.candidates.map(candidate => ( - + {[...availableGroups].map(([provider, group]) => ( +
+
{group.providerName}
+ {group.candidates.map(renderCandidate)} +
))} + {unavailable.length > 0 + ? ( +
+
{t('subagentModelSelectionUnavailableGroup')}
+ {unavailable.map(renderCandidate)} +
+ ) + : null}
) : state.catalogStatus === 'ready' @@ -94,7 +131,6 @@ export function SubagentModelSelectionCard(props: SubagentModelSelectionCardProp ) :

{t('subagentModelSelectionOff')}

} - {state.saved ?

{t('subagentModelSelectionSaved')}

: null}
) } diff --git a/packages/client/ui-settings-plugins/src/client/locales.ts b/packages/client/ui-settings-plugins/src/client/locales.ts index 24a3424a01..018d87d47d 100644 --- a/packages/client/ui-settings-plugins/src/client/locales.ts +++ b/packages/client/ui-settings-plugins/src/client/locales.ts @@ -14,8 +14,9 @@ export type PluginsSettingsLocaleKey = | 'subagentModelSelectionTitle' | 'subagentModelSelectionDescription' | 'subagentModelSelectionToggle' | 'subagentModelSelectionChoose' | 'subagentModelSelectionAllowed' | 'subagentModelSelectionLoading' | 'subagentModelSelectionLoadFailed' | 'subagentModelSelectionRetry' - | 'subagentModelSelectionPartial' | 'subagentModelSelectionUnavailable' | 'subagentModelSelectionEmpty' - | 'subagentModelSelectionRequired' | 'subagentModelSelectionOff' | 'subagentModelSelectionSaved' + | 'subagentModelSelectionPartial' | 'subagentModelSelectionUnavailable' + | 'subagentModelSelectionUnavailableGroup' | 'subagentModelSelectionEmpty' + | 'subagentModelSelectionRequired' | 'subagentModelSelectionOff' /** English copy. */ export const en: Record = { @@ -56,20 +57,20 @@ export const en: Record = { webSearchBaseUrlHint: 'Leave blank to use the provider default.', webSearchMaxUses: 'Max searches per request', webSearchMaxUsesHint: 'How many times one request may search before it must answer.', - subagentModelSelectionTitle: 'Subagent model selection', - subagentModelSelectionDescription: 'Choose which child models new sessions may select. Running sessions do not change.', - subagentModelSelectionToggle: 'Allow subagents to choose models', - subagentModelSelectionChoose: 'Select at least one model. Only these adapter routes appear in subagent discovery.', - subagentModelSelectionAllowed: 'Allowed models', - subagentModelSelectionLoading: 'Loading adapter models…', - subagentModelSelectionLoadFailed: 'Adapter models could not be loaded.', + subagentModelSelectionTitle: 'Subagent', + subagentModelSelectionDescription: 'Control which models agents may choose for subagents.', + subagentModelSelectionToggle: 'Allow agents to choose models for subagents', + subagentModelSelectionChoose: 'When enabled, agents can choose a provider, model, and reasoning effort for each subagent from the authorized models below. Applies only to new sessions.', + subagentModelSelectionAllowed: 'Models agents may choose', + subagentModelSelectionLoading: 'Loading models…', + subagentModelSelectionLoadFailed: 'Models could not be loaded.', subagentModelSelectionRetry: 'Retry', - subagentModelSelectionPartial: 'Some providers could not list their models; stored choices remain removable.', - subagentModelSelectionUnavailable: 'Unavailable', - subagentModelSelectionEmpty: 'No adapter currently advertises a model.', + subagentModelSelectionPartial: 'Some model providers could not be loaded; saved choices remain removable.', + subagentModelSelectionUnavailable: 'Currently unavailable', + subagentModelSelectionUnavailableGroup: 'Saved but currently unavailable', + subagentModelSelectionEmpty: 'No model provider currently advertises a model.', subagentModelSelectionRequired: 'Select at least one model before saving.', - subagentModelSelectionOff: 'New sessions inherit the configured or parent model without choosing another route.', - subagentModelSelectionSaved: 'Saved. New sessions use this setting.', + subagentModelSelectionOff: 'Subagents use configured defaults or inherit the parent agent\'s model. Saved model choices are retained.', } /** Simplified Chinese copy. */ @@ -111,18 +112,18 @@ export const zh: Record = { webSearchBaseUrlHint: '留空则使用提供方默认地址。', webSearchMaxUses: '单次请求最多搜索次数', webSearchMaxUsesHint: '一次请求在必须作答前最多可以搜索多少次。', - subagentModelSelectionTitle: 'Subagent 自选模型', - subagentModelSelectionDescription: '选择新会话允许为 subagent 自选的模型。运行中的会话不会改变。', - subagentModelSelectionToggle: '允许 subagent 自选模型', - subagentModelSelectionChoose: '请至少选择一个模型。Subagent 发现工具只会列出这些 adapter 路由。', - subagentModelSelectionAllowed: '允许的模型', - subagentModelSelectionLoading: '正在加载 adapter 模型…', - subagentModelSelectionLoadFailed: '无法加载 adapter 模型。', + subagentModelSelectionTitle: 'Subagent', + subagentModelSelectionDescription: '控制 Agent 为 Subagent 选择模型的权限。', + subagentModelSelectionToggle: '允许 Agent 为 Subagent 选择模型', + subagentModelSelectionChoose: '开启后,Agent 可以从下方授权模型中,为每个 Subagent 选择提供方、模型和推理强度。仅影响新会话。', + subagentModelSelectionAllowed: 'Agent 可选择的模型', + subagentModelSelectionLoading: '正在加载模型…', + subagentModelSelectionLoadFailed: '无法加载模型。', subagentModelSelectionRetry: '重试', - subagentModelSelectionPartial: '部分提供方无法列出模型;仍可移除已保存的选项。', - subagentModelSelectionUnavailable: '不可用', - subagentModelSelectionEmpty: '当前没有 adapter 公布模型。', + subagentModelSelectionPartial: '部分模型提供方暂时无法加载;已保存的选择仍可移除。', + subagentModelSelectionUnavailable: '当前不可用', + subagentModelSelectionUnavailableGroup: '已保存但当前不可用', + subagentModelSelectionEmpty: '当前没有模型提供方公布模型。', subagentModelSelectionRequired: '保存前请至少选择一个模型。', - subagentModelSelectionOff: '新会话会使用配置值或继承父 Agent 模型,不会自主选择其他路由。', - subagentModelSelectionSaved: '已保存,新会话将使用此设置。', + subagentModelSelectionOff: '关闭后,Subagent 使用配置的默认模型或继承父 Agent 的模型;已选模型会保留。', } diff --git a/packages/client/ui-settings-plugins/src/client/subagent-model-selection-card-controller.ts b/packages/client/ui-settings-plugins/src/client/subagent-model-selection-card-controller.ts index f9a86396a8..8af7361f13 100644 --- a/packages/client/ui-settings-plugins/src/client/subagent-model-selection-card-controller.ts +++ b/packages/client/ui-settings-plugins/src/client/subagent-model-selection-card-controller.ts @@ -2,7 +2,6 @@ import type { IApiClient, - ModelCatalogFailure, ModelProviderGroup, } from '@deepseek-ai/dsh-api-remotes/client' import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store' @@ -48,10 +47,8 @@ export interface SubagentModelSelectionCardState extends CardShell { candidates: readonly SubagentModelCandidate[] /** Adapter-directory request state. */ catalogStatus: 'idle' | 'loading' | 'ready' | 'error' - /** Provider-local failures that did not block other candidates. */ - catalogFailures: readonly ModelCatalogFailure[] - /** Whether the latest save landed. */ - saved: boolean + /** Whether any provider-local catalog request failed. */ + catalogPartial: boolean } /** Registration-side face for the subagent model-selection card. */ @@ -130,13 +127,12 @@ function sameRoutes(left: readonly AllowedSubagentModel[], right: readonly Allow /** Bridges one settings scope and the live adapter directory onto a staged card. */ export class SubagentModelSelectionCardController { private catalogGroups: readonly ModelProviderGroup[] = [] - private catalogFailures: readonly ModelCatalogFailure[] = [] + private catalogPartial = false private catalogStatus: SubagentModelSelectionCardState['catalogStatus'] = 'idle' private draftEnabled: boolean | undefined private draftSelected: Set | undefined private draftRevision: number | undefined private saving = false - private saved = false private failed = false private disposed = false private saveGeneration = 0 @@ -156,7 +152,6 @@ export class SubagentModelSelectionCardController { this.unsubscribe = scope.subscribe(() => { if (!this.saving && this.draftSelected !== undefined && this.scope.getSnapshot().revision !== this.draftRevision) { - this.saved = false this.failed = true } if (this.enabled() && this.catalogStatus === 'idle') void this.loadCatalog() @@ -219,7 +214,6 @@ export class SubagentModelSelectionCardController { if (this.disposed || snapshot.status !== 'ready' || !snapshot.writable || this.saving) return this.beginDraft() this.draftEnabled = !this.draftEnabled - this.saved = false this.failed = false if (this.draftEnabled && this.catalogStatus === 'idle') void this.loadCatalog() this.publish() @@ -231,7 +225,6 @@ export class SubagentModelSelectionCardController { const selected = this.beginDraft() if (selected.has(key)) selected.delete(key) else selected.add(key) - this.saved = false this.failed = false this.publish() } @@ -241,7 +234,6 @@ export class SubagentModelSelectionCardController { this.draftEnabled = undefined this.draftSelected = undefined this.draftRevision = undefined - this.saved = false this.failed = false this.publish() } @@ -264,14 +256,12 @@ export class SubagentModelSelectionCardController { || (this.currentEnabled() === desiredEnabled && sameRoutes(this.currentRoutes(), desired)) || (desiredEnabled && desired.length === 0)) return if (this.draftSelected !== undefined && snapshot.revision !== this.draftRevision) { - this.saved = false this.failed = true this.publish() return } const generation = this.saveGeneration this.saving = true - this.saved = false this.failed = false this.publish() await this.scope.mutate([ @@ -281,7 +271,6 @@ export class SubagentModelSelectionCardController { if (generation !== this.saveGeneration) return const landed = this.currentEnabled() === desiredEnabled && sameRoutes(this.currentRoutes(), desired) this.saving = false - this.saved = landed this.failed = !landed if (landed) { this.draftEnabled = undefined @@ -296,7 +285,7 @@ export class SubagentModelSelectionCardController { if (this.disposed) return this.catalogGeneration += 1 this.catalogStatus = 'idle' - this.catalogFailures = [] + this.catalogPartial = false if (this.enabled()) void this.loadCatalog() else this.publish() } @@ -313,14 +302,14 @@ export class SubagentModelSelectionCardController { const generation = this.catalogGeneration this.catalogStatus = 'loading' this.catalogGroups = [] - this.catalogFailures = [] + this.catalogPartial = false this.publish() try { const response = await this.api.llm.models({}) if (generation !== this.catalogGeneration) return if (!response.result.ok) throw new Error(response.result.error.message) this.catalogGroups = response.result.value.groups - this.catalogFailures = response.result.value.failures + this.catalogPartial = response.result.value.failures.length > 0 this.catalogStatus = 'ready' } catch { if (generation !== this.catalogGeneration) return @@ -344,8 +333,7 @@ export class SubagentModelSelectionCardController { enabled, candidates: this.candidates(), catalogStatus: this.catalogStatus, - catalogFailures: this.catalogFailures, - saved: this.saved, + catalogPartial: this.catalogPartial, } } diff --git a/packages/client/ui-settings-plugins/tests/section.client.spec.tsx b/packages/client/ui-settings-plugins/tests/section.client.spec.tsx index 46709d057f..7effc16dee 100644 --- a/packages/client/ui-settings-plugins/tests/section.client.spec.tsx +++ b/packages/client/ui-settings-plugins/tests/section.client.spec.tsx @@ -88,8 +88,7 @@ function renderSubagentModelSelection(state: Partial { expect(actions.toggleEnabled).toHaveBeenCalledOnce() }) - it('renders adapter candidates and reports a successful save', () => { + it('groups available adapter candidates by provider', () => { const actions = renderSubagentModelSelection({ enabled: true, - saved: true, candidates: [{ key: 'alpha\0fast', provider: 'alpha', @@ -353,7 +351,7 @@ describe('SubagentModelSelectionCard', () => { fireEvent.click(screen.getByText(en.subagentModelSelectionTitle)) expect(screen.getByRole('switch').getAttribute('aria-checked')).toBe('true') - expect(screen.getByRole('status').textContent).toBe(en.subagentModelSelectionSaved) + expect(screen.getByText('Alpha API', { exact: true })).toBeTruthy() fireEvent.click(screen.getByRole('checkbox', { name: /Fast/ })) expect(actions.toggleModel).toHaveBeenCalledWith('alpha\0fast') }) @@ -374,7 +372,7 @@ describe('SubagentModelSelectionCard', () => { renderSubagentModelSelection({ enabled: true, catalogStatus: 'ready', - catalogFailures: [{ id: 'beta', name: 'Beta', message: 'offline' }], + catalogPartial: true, candidates: [{ key: 'legacy\0old', provider: 'legacy', @@ -388,6 +386,7 @@ describe('SubagentModelSelectionCard', () => { fireEvent.click(screen.getByText(en.subagentModelSelectionTitle)) expect(screen.getByText(en.subagentModelSelectionPartial)).toBeTruthy() expect(screen.getByText(en.subagentModelSelectionUnavailable)).toBeTruthy() + expect(screen.getByText(en.subagentModelSelectionUnavailableGroup)).toBeTruthy() cleanup() renderSubagentModelSelection({ enabled: true, catalogStatus: 'ready' }) diff --git a/packages/client/ui-settings-plugins/tests/stores.client.spec.ts b/packages/client/ui-settings-plugins/tests/stores.client.spec.ts index 4b1a8317e8..3e5002954d 100644 --- a/packages/client/ui-settings-plugins/tests/stores.client.spec.ts +++ b/packages/client/ui-settings-plugins/tests/stores.client.spec.ts @@ -480,7 +480,6 @@ describe('SubagentModelSelectionCardController', () => { enabled: true, dirty: false, saving: false, - saved: true, failed: false, }) }) @@ -521,7 +520,6 @@ describe('SubagentModelSelectionCardController', () => { enabled: true, dirty: true, saving: false, - saved: false, }) }) @@ -539,6 +537,7 @@ describe('SubagentModelSelectionCardController', () => { const face = controller.inject() const state = () => face.hooks.subagentModelSelectionCard.getSnapshot() await vi.waitFor(() => { expect(state().catalogStatus).toBe('ready') }) + expect(state().catalogPartial).toBe(true) face.toggleModel('missing') expect(state().dirty).toBe(false) @@ -576,7 +575,7 @@ describe('SubagentModelSelectionCardController', () => { ], 5) }) expect(face.hooks.subagentModelSelectionCard.getSnapshot()).toMatchObject({ - enabled: false, dirty: false, saved: true, + enabled: false, dirty: false, }) })