From 65e662450c7638632a2c27f8569ce89c70308b27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Mon, 15 Jun 2026 23:00:20 +0800 Subject: [PATCH] fix(desktop): default new sessions to provider main model (#818) Tested: bun run check:desktop Confidence: high Scope-risk: narrow --- .../controls/ModelSelector.test.tsx | 60 ++++++++++++++++--- .../src/components/controls/ModelSelector.tsx | 14 +++-- desktop/src/stores/providerStore.test.ts | 16 +++++ desktop/src/stores/providerStore.ts | 11 +++- 4 files changed, 87 insertions(+), 14 deletions(-) diff --git a/desktop/src/components/controls/ModelSelector.test.tsx b/desktop/src/components/controls/ModelSelector.test.tsx index 25495a67..84c318c8 100644 --- a/desktop/src/components/controls/ModelSelector.test.tsx +++ b/desktop/src/components/controls/ModelSelector.test.tsx @@ -97,11 +97,12 @@ describe('ModelSelector', () => { render() - await clickByRole(/alpha/i) + await clickByRole(/provider-main/i) await act(async () => { + fireEvent.keyDown(document, { key: 'Escape' }) await Promise.resolve() }) - await clickByRole(/alpha/i) + await clickByRole(/provider-main/i) expect(fetchClaudeStatus).toHaveBeenCalledTimes(1) expect(fetchOpenAIStatus).toHaveBeenCalledTimes(1) @@ -192,7 +193,7 @@ describe('ModelSelector', () => { render() - await clickByRole(/alpha/i) + await clickByRole(/provider-main/i) await act(async () => { fireEvent.click(screen.getByRole('button', { name: /provider-fast/ })) await Promise.resolve() @@ -210,6 +211,51 @@ describe('ModelSelector', () => { }) }) + it('defaults blank provider-scoped runtime selections to the active provider main model', async () => { + useSettingsStore.setState({ + locale: 'en', + availableModels: [ + { id: 'deepseek-v4-flash', name: 'deepseek-v4-flash', description: 'Main Model · Haiku Model', context: '' }, + { id: 'deepseek-v4-pro', name: 'deepseek-v4-pro', description: 'Sonnet Model · Opus Model', context: '' }, + ], + currentModel: { id: 'deepseek-v4-pro', name: 'deepseek-v4-pro', description: 'Sonnet Model · Opus Model', context: '' }, + activeProviderName: 'Custom-DeepSeek-OpenAI', + }) + useProviderStore.setState({ + providers: [{ + id: 'deepseek-provider', + presetId: 'custom', + name: 'Custom-DeepSeek-OpenAI', + apiKey: '***', + baseUrl: 'https://api.deepseek.com', + apiFormat: 'openai_chat', + models: { + main: 'deepseek-v4-flash', + haiku: 'deepseek-v4-flash', + sonnet: 'deepseek-v4-pro', + opus: 'deepseek-v4-pro', + }, + }], + activeId: 'deepseek-provider', + hasLoadedProviders: true, + isLoading: true, + }) + + render() + + const trigger = screen.getByRole('button', { name: /deepseek-v4-flash/i }) + await act(async () => { + fireEvent.click(trigger) + await Promise.resolve() + }) + + const flashOption = screen + .getAllByRole('button', { name: /deepseek-v4-flash/i }) + .find((button) => button.textContent?.includes('Main Model')) + expect(flashOption).toBeDefined() + expect(flashOption?.className).toContain('border-[var(--color-model-option-selected-border)]') + }) + it('keeps runtime effort scoped to the selected session', async () => { const setSessionRuntime = vi.fn() useSettingsStore.setState({ @@ -249,12 +295,12 @@ describe('ModelSelector', () => { render() - await clickByRole(/alpha/i) + await clickByRole(/provider-main/i) await clickByRole(/^High$/) expect(useSessionRuntimeStore.getState().selections['session-1']).toEqual({ providerId: 'provider-a', - modelId: 'alpha', + modelId: 'provider-main', effortLevel: 'high', }) expect(useSessionRuntimeStore.getState().selections['session-2']).toEqual({ @@ -264,7 +310,7 @@ describe('ModelSelector', () => { }) expect(setSessionRuntime).toHaveBeenCalledWith('session-1', { providerId: 'provider-a', - modelId: 'alpha', + modelId: 'provider-main', effortLevel: 'high', }) expect(useSettingsStore.getState().effortLevel).toBe('max') @@ -357,7 +403,7 @@ describe('ModelSelector', () => { render() - await clickByRole(/alpha/i) + await clickByRole(/provider-main/i) const dropdown = screen.getByTestId('model-selector-dropdown') expect(dropdown.textContent).not.toContain('Claude Official') diff --git a/desktop/src/components/controls/ModelSelector.tsx b/desktop/src/components/controls/ModelSelector.tsx index 392e2c5c..306ea2a0 100644 --- a/desktop/src/components/controls/ModelSelector.tsx +++ b/desktop/src/components/controls/ModelSelector.tsx @@ -146,15 +146,17 @@ function resolveDefaultRuntimeSelection( providers: SavedProvider[], currentModelId: string | undefined, ): RuntimeSelection { - const inferredProviderId = activeId ?? ( - activeProviderName - ? providers.find((provider) => provider.name === activeProviderName)?.id ?? null - : null - ) + const activeProvider = activeId + ? providers.find((provider) => provider.id === activeId) + : activeProviderName + ? providers.find((provider) => provider.name === activeProviderName) + : undefined + const inferredProviderId = activeId ?? activeProvider?.id ?? null + const providerMainModelId = activeProvider?.models.main.trim() return { providerId: inferredProviderId, - modelId: currentModelId ?? ( + modelId: providerMainModelId || currentModelId || ( inferredProviderId === OPENAI_OFFICIAL_PROVIDER_ID ? OPENAI_OFFICIAL_DEFAULT_MODEL_ID : OFFICIAL_DEFAULT_MODEL_ID diff --git a/desktop/src/stores/providerStore.test.ts b/desktop/src/stores/providerStore.test.ts index e66149da..20795170 100644 --- a/desktop/src/stores/providerStore.test.ts +++ b/desktop/src/stores/providerStore.test.ts @@ -115,6 +115,7 @@ describe('providerStore runtime refresh', () => { providerId: provider.id, modelId: 'model-main', }) + expect(settingsSetModelMock).not.toHaveBeenCalled() }) it('keeps an explicit provider model selection when the model still exists', async () => { @@ -181,6 +182,21 @@ describe('providerStore runtime refresh', () => { expect(settingsSetModelMock).toHaveBeenCalledWith('model-main') expect(settingsFetchAllMock).toHaveBeenCalled() }) + + it('sets the provider main model when updating the active saved provider', async () => { + const provider = makeProvider({ models: { main: 'model-flash', haiku: 'model-flash', sonnet: 'model-pro', opus: 'model-pro' } }) + providersApiMock.update.mockResolvedValue({ provider }) + providersApiMock.list.mockResolvedValue({ + providers: [provider], + activeId: provider.id, + }) + + const { useProviderStore } = await import('./providerStore') + await useProviderStore.getState().updateProvider(provider.id, { models: provider.models }) + + expect(settingsSetModelMock).toHaveBeenCalledWith('model-flash') + expect(settingsFetchAllMock).toHaveBeenCalled() + }) }) describe('providerStore reorderProviders', () => { diff --git a/desktop/src/stores/providerStore.ts b/desktop/src/stores/providerStore.ts index b1912a8a..ffb68d6d 100644 --- a/desktop/src/stores/providerStore.ts +++ b/desktop/src/stores/providerStore.ts @@ -203,7 +203,16 @@ export const useProviderStore = create((set, get) => ({ updateProvider: async (id, input) => { const { provider } = await providersApi.update(id, input) await get().fetchProviders() - refreshConnectedSessionsForProvider(provider, get().activeId) + const activeId = get().activeId + if (activeId === provider.id && input.models !== undefined) { + const mainModelId = provider.models.main.trim() + if (mainModelId) { + const settings = useSettingsStore.getState() + await settings.setModel(mainModelId) + await settings.fetchAll() + } + } + refreshConnectedSessionsForProvider(provider, activeId) return provider },