fix(ui-settings-plugins): align Subagent configuration card

This commit is contained in:
Dudu-0223
2026-08-27 12:00:02 +08:00
parent a130273434
commit 0b2f476071
8 changed files with 124 additions and 87 deletions
@@ -25,8 +25,8 @@
- tabpanel "插件配置":
- list:
- listitem:
- 'button "展开设置: Subagent 自选模型"':
- text: Subagent 自选模型 选择新会话允许subagent 自选的模型。运行中的会话不会改变
- 'button "展开设置: Subagent"':
- text: Subagent 控制 Agent Subagent 选择模型的权限
- img
- listitem:
- 'button "展开设置: 终端"':
+7 -6
View File
@@ -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()
@@ -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;
@@ -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<string, {
providerName: string
candidates: SubagentModelCandidate[]
}>()
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) => (
<label key={candidate.key} className={css.model}>
<input
type="checkbox"
checked={candidate.selected}
disabled={!state.writable || state.saving}
onChange={() => { props.toggleModel(candidate.key) }}
/>
<span>
<span className={css.modelName}>{candidate.modelName}</span>
<span className={css.route}>{`${candidate.providerName} · ${candidate.provider}/${candidate.model}`}</span>
</span>
{!candidate.available
? <span className={css.unavailable}>{t('subagentModelSelectionUnavailable')}</span>
: null}
</label>
)
return (
<PluginCard
t={t}
@@ -61,30 +101,27 @@ export function SubagentModelSelectionCard(props: SubagentModelSelectionCardProp
</div>
)
: null}
{state.catalogFailures.length > 0
{state.catalogPartial
? <p className={css.notice}>{t('subagentModelSelectionPartial')}</p>
: null}
{state.candidates.length > 0
? (
<fieldset className={css.models}>
<legend>{t('subagentModelSelectionAllowed')}</legend>
{state.candidates.map(candidate => (
<label key={candidate.key} className={css.model}>
<input
type="checkbox"
checked={candidate.selected}
disabled={!state.writable || state.saving}
onChange={() => { props.toggleModel(candidate.key) }}
/>
<span>
<span className={css.modelName}>{candidate.modelName}</span>
<span className={css.route}>{`${candidate.providerName} · ${candidate.provider}/${candidate.model}`}</span>
</span>
{!candidate.available
? <span className={css.unavailable}>{t('subagentModelSelectionUnavailable')}</span>
: null}
</label>
{[...availableGroups].map(([provider, group]) => (
<div key={provider} className={css.modelGroup}>
<div className={css.providerName}>{group.providerName}</div>
{group.candidates.map(renderCandidate)}
</div>
))}
{unavailable.length > 0
? (
<div className={css.modelGroup}>
<div className={css.providerName}>{t('subagentModelSelectionUnavailableGroup')}</div>
{unavailable.map(renderCandidate)}
</div>
)
: null}
</fieldset>
)
: state.catalogStatus === 'ready'
@@ -94,7 +131,6 @@ export function SubagentModelSelectionCard(props: SubagentModelSelectionCardProp
</div>
)
: <p className={css.hint}>{t('subagentModelSelectionOff')}</p>}
{state.saved ? <p className={css.status} role="status">{t('subagentModelSelectionSaved')}</p> : null}
</PluginCard>
)
}
@@ -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<PluginsSettingsLocaleKey, string> = {
@@ -56,20 +57,20 @@ export const en: Record<PluginsSettingsLocaleKey, string> = {
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<PluginsSettingsLocaleKey, string> = {
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 模型;已选模型会保留。',
}
@@ -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<string> | 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,
}
}
@@ -88,8 +88,7 @@ function renderSubagentModelSelection(state: Partial<SubagentModelSelectionCardS
enabled: false,
candidates: [],
catalogStatus: 'idle',
catalogFailures: [],
saved: false,
catalogPartial: false,
...state,
})
const actions = {
@@ -335,10 +334,9 @@ describe('SubagentModelSelectionCard', () => {
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' })
@@ -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,
})
})