From ba8917f1db007a6bc42dc7976235e4a261fccf70 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, 20 Jul 2026 20:38:51 +0800 Subject: [PATCH] fix(desktop): decouple provider creation from presets request #1078 --- desktop/src/__tests__/agentsSettings.test.tsx | 2 -- .../__tests__/diagnosticsSettings.test.tsx | 2 -- .../src/__tests__/generalSettings.test.tsx | 19 ++++++++++++---- .../src/__tests__/pluginsSettings.test.tsx | 2 -- desktop/src/__tests__/skillsSettings.test.tsx | 2 -- desktop/src/api/providers.ts | 6 ----- desktop/src/config/providerPresets.ts | 6 +++++ desktop/src/pages/Settings.tsx | 22 +++++++------------ desktop/src/stores/providerStore.test.ts | 12 +++++++++- desktop/src/stores/providerStore.ts | 16 ++------------ desktop/tsconfig.json | 1 + 11 files changed, 43 insertions(+), 47 deletions(-) create mode 100644 desktop/src/config/providerPresets.ts diff --git a/desktop/src/__tests__/agentsSettings.test.tsx b/desktop/src/__tests__/agentsSettings.test.tsx index a186bc31..9550f5ee 100644 --- a/desktop/src/__tests__/agentsSettings.test.tsx +++ b/desktop/src/__tests__/agentsSettings.test.tsx @@ -24,9 +24,7 @@ vi.mock('../stores/providerStore', () => ({ activeId: null, presets: [], isLoading: false, - isPresetsLoading: false, fetchProviders: vi.fn(), - fetchPresets: vi.fn(), deleteProvider: vi.fn(), activateProvider: vi.fn(), activateOfficial: vi.fn(), diff --git a/desktop/src/__tests__/diagnosticsSettings.test.tsx b/desktop/src/__tests__/diagnosticsSettings.test.tsx index 023adec3..b08fe2f5 100644 --- a/desktop/src/__tests__/diagnosticsSettings.test.tsx +++ b/desktop/src/__tests__/diagnosticsSettings.test.tsx @@ -94,9 +94,7 @@ vi.mock('../stores/providerStore', () => ({ hasLoadedProviders: true, presets: [], isLoading: false, - isPresetsLoading: false, fetchProviders: vi.fn(), - fetchPresets: vi.fn(), deleteProvider: vi.fn(), activateProvider: vi.fn(), activateOfficial: vi.fn(), diff --git a/desktop/src/__tests__/generalSettings.test.tsx b/desktop/src/__tests__/generalSettings.test.tsx index d895ba39..96446312 100644 --- a/desktop/src/__tests__/generalSettings.test.tsx +++ b/desktop/src/__tests__/generalSettings.test.tsx @@ -40,9 +40,7 @@ const providerStoreState = { hasLoadedProviders: true, presets: [] as ProviderPreset[], isLoading: false, - isPresetsLoading: false, fetchProviders: vi.fn(), - fetchPresets: vi.fn(), deleteProvider: MOCK_DELETE_PROVIDER, activateProvider: vi.fn(), activateOfficial: vi.fn(), @@ -199,9 +197,7 @@ describe('Settings > General tab', () => { providerStoreState.hasLoadedProviders = true providerStoreState.presets = [] providerStoreState.isLoading = false - providerStoreState.isPresetsLoading = false providerStoreState.fetchProviders = vi.fn() - providerStoreState.fetchPresets = vi.fn() providerStoreState.activateProvider = vi.fn() providerStoreState.activateOfficial = vi.fn() providerStoreState.testProvider = vi.fn() @@ -1687,6 +1683,21 @@ describe('Settings > Providers tab', () => { expect(MOCK_DELETE_PROVIDER).toHaveBeenCalledWith('provider-1') }) + it('keeps custom provider creation available when presets are unavailable', () => { + providerStoreState.presets = [] + + render() + + const addButton = screen.getByRole('button', { name: /Add Provider/i }) + expect(addButton).toBeEnabled() + + fireEvent.click(addButton) + + const dialog = screen.getByRole('dialog') + expect(within(dialog).getByLabelText(/Name/i)).toHaveValue('Custom') + expect(within(dialog).getByLabelText(/Base URL/i)).toBeEnabled() + }) + it('uses the shared dropdown for API format in the provider form', () => { providerStoreState.presets = [ { diff --git a/desktop/src/__tests__/pluginsSettings.test.tsx b/desktop/src/__tests__/pluginsSettings.test.tsx index 010bda8d..6c174e0f 100644 --- a/desktop/src/__tests__/pluginsSettings.test.tsx +++ b/desktop/src/__tests__/pluginsSettings.test.tsx @@ -25,9 +25,7 @@ vi.mock('../stores/providerStore', () => ({ activeId: null, presets: [], isLoading: false, - isPresetsLoading: false, fetchProviders: vi.fn(), - fetchPresets: vi.fn(), deleteProvider: vi.fn(), activateProvider: vi.fn(), activateOfficial: vi.fn(), diff --git a/desktop/src/__tests__/skillsSettings.test.tsx b/desktop/src/__tests__/skillsSettings.test.tsx index d1aa1eba..4674a484 100644 --- a/desktop/src/__tests__/skillsSettings.test.tsx +++ b/desktop/src/__tests__/skillsSettings.test.tsx @@ -21,9 +21,7 @@ vi.mock('../stores/providerStore', () => ({ activeId: null, presets: [], isLoading: false, - isPresetsLoading: false, fetchProviders: vi.fn(), - fetchPresets: vi.fn(), deleteProvider: vi.fn(), activateProvider: vi.fn(), activateOfficial: vi.fn(), diff --git a/desktop/src/api/providers.ts b/desktop/src/api/providers.ts index 00adabeb..f9a12bfd 100644 --- a/desktop/src/api/providers.ts +++ b/desktop/src/api/providers.ts @@ -8,13 +8,11 @@ import type { TestProviderConfigInput, ProviderTestResult, } from '../types/provider' -import type { ProviderPreset } from '../types/providerPreset' type ProvidersResponse = { providers: SavedProvider[]; activeId: string | null } type ProvidersListResponse = ProvidersResponse & { providerOrder?: string[] } type ProvidersReorderResponse = { providers: SavedProvider[]; providerOrder?: string[] } type ProviderResponse = { provider: SavedProvider } -type PresetsResponse = { presets: ProviderPreset[] } type TestResultResponse = { result: ProviderTestResult } type AuthStatusResponse = { hasAuth: boolean @@ -27,10 +25,6 @@ export const providersApi = { return api.get('/api/providers') }, - presets() { - return api.get('/api/providers/presets') - }, - authStatus() { return api.get('/api/providers/auth-status') }, diff --git a/desktop/src/config/providerPresets.ts b/desktop/src/config/providerPresets.ts new file mode 100644 index 00000000..b5fcee83 --- /dev/null +++ b/desktop/src/config/providerPresets.ts @@ -0,0 +1,6 @@ +import providerPresetsJson from '../../../src/server/config/providerPresets.json' +import type { ProviderPreset } from '../types/providerPreset' + +// Presets ship with the desktop bundle. Provider creation must remain available +// even when the local HTTP control plane is temporarily unavailable. +export const BUNDLED_PROVIDER_PRESETS = providerPresetsJson as ProviderPreset[] diff --git a/desktop/src/pages/Settings.tsx b/desktop/src/pages/Settings.tsx index 1469d507..406f7c22 100644 --- a/desktop/src/pages/Settings.tsx +++ b/desktop/src/pages/Settings.tsx @@ -361,9 +361,7 @@ function ProviderSettings() { hasLoadedProviders, presets, isLoading, - isPresetsLoading, fetchProviders, - fetchPresets, deleteProvider, reorderProviders, activateProvider, @@ -388,8 +386,7 @@ function ProviderSettings() { useEffect(() => { void fetchProviders() - void fetchPresets() - }, [fetchPresets, fetchProviders]) + }, [fetchProviders]) const presetMap = useMemo( () => new Map(presets.map((preset) => [preset.id, preset])), @@ -462,7 +459,7 @@ function ProviderSettings() {

{t('settings.providers.title')}

{t('settings.providers.description')}

- @@ -1211,21 +1208,18 @@ function ProviderFormModal({ open, onClose, mode, provider, presets }: ProviderF const fetchSettings = useSettingsStore((s) => s.fetchAll) const t = useTranslation() - const availablePresets = presets.filter((p) => p.id !== 'official') + const fallbackPreset = buildFallbackPreset(provider) + const loadedPresets = presets.filter((p) => p.id !== 'official') + const availablePresets = loadedPresets.length > 0 ? loadedPresets : [fallbackPreset] const regularPresets = availablePresets.filter((p) => !p.featured) const featuredPresets = availablePresets.filter((p) => p.featured) const presetDefaultEnvKeys = useMemo( () => presets.flatMap((preset) => Object.keys(preset.defaultEnv ?? {})), [presets], ) - const fallbackPreset = provider - ? buildFallbackPreset(provider) - : requirePreset(availablePresets[availablePresets.length - 1]) - const initialPreset = requirePreset( - provider - ? availablePresets.find((p) => p.id === provider.presetId) ?? fallbackPreset - : availablePresets[0] ?? fallbackPreset, - ) + const initialPreset = provider + ? availablePresets.find((p) => p.id === provider.presetId) ?? fallbackPreset + : availablePresets[0] ?? fallbackPreset const initialModels = stripModel1mMarkers(provider?.models ?? initialPreset.defaultModels) const initialModel1mSupport = getInitialModel1mSupport( provider?.models ?? initialPreset.defaultModels, diff --git a/desktop/src/stores/providerStore.test.ts b/desktop/src/stores/providerStore.test.ts index 7fa15cb8..232fb38a 100644 --- a/desktop/src/stores/providerStore.test.ts +++ b/desktop/src/stores/providerStore.test.ts @@ -12,7 +12,6 @@ const { } = vi.hoisted(() => ({ providersApiMock: { list: vi.fn(), - presets: vi.fn(), authStatus: vi.fn(), getSettings: vi.fn(), updateSettings: vi.fn(), @@ -88,6 +87,17 @@ function makeProvider(overrides: Partial = {}): SavedProvider { } } +describe('providerStore presets', () => { + it('starts with the provider presets bundled into the desktop app', async () => { + const { useProviderStore } = await import('./providerStore') + + expect(useProviderStore.getState().presets).toEqual(expect.arrayContaining([ + expect.objectContaining({ id: 'custom' }), + expect.objectContaining({ id: 'deepseek' }), + ])) + }) +}) + describe('providerStore runtime refresh', () => { beforeEach(() => { vi.clearAllMocks() diff --git a/desktop/src/stores/providerStore.ts b/desktop/src/stores/providerStore.ts index 728595a9..fd0f5702 100644 --- a/desktop/src/stores/providerStore.ts +++ b/desktop/src/stores/providerStore.ts @@ -15,6 +15,7 @@ import { GROK_OFFICIAL_DEFAULT_MODEL_ID, GROK_OFFICIAL_PROVIDER_ID, } from '../constants/grokOfficialProvider' +import { BUNDLED_PROVIDER_PRESETS } from '../config/providerPresets' import type { SavedProvider, CreateProviderInput, @@ -32,11 +33,9 @@ type ProviderStore = { hasLoadedProviders: boolean presets: ProviderPreset[] isLoading: boolean - isPresetsLoading: boolean error: string | null fetchProviders: () => Promise - fetchPresets: () => Promise createProvider: (input: CreateProviderInput) => Promise updateProvider: (id: string, input: UpdateProviderInput) => Promise deleteProvider: (id: string) => Promise @@ -164,9 +163,8 @@ export const useProviderStore = create((set, get) => ({ providerOrder: [...BUILT_IN_PROVIDER_IDS], activeId: null, hasLoadedProviders: false, - presets: [], + presets: BUNDLED_PROVIDER_PRESETS, isLoading: false, - isPresetsLoading: false, error: null, fetchProviders: async () => { @@ -188,16 +186,6 @@ export const useProviderStore = create((set, get) => ({ } }, - fetchPresets: async () => { - set({ isPresetsLoading: true, error: null }) - try { - const { presets } = await providersApi.presets() - set({ presets, isPresetsLoading: false }) - } catch (err) { - set({ isPresetsLoading: false, error: err instanceof Error ? err.message : String(err) }) - } - }, - createProvider: async (input) => { const { provider } = await providersApi.create(input) await get().fetchProviders() diff --git a/desktop/tsconfig.json b/desktop/tsconfig.json index be91bcd8..ab993ee5 100644 --- a/desktop/tsconfig.json +++ b/desktop/tsconfig.json @@ -6,6 +6,7 @@ "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", + "resolveJsonModule": true, "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force",