From 1178c4e4f584c8de2a41f387d135586879d35975 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Sun, 28 Jun 2026 13:05:00 +0300 Subject: [PATCH] fix(providers): keep add-provider flow selected during sync --- packages/ui/src/stores/useConfigStore.test.ts | 66 +++++++++++++++++++ packages/ui/src/stores/useConfigStore.ts | 38 ++++++++--- 2 files changed, 94 insertions(+), 10 deletions(-) diff --git a/packages/ui/src/stores/useConfigStore.test.ts b/packages/ui/src/stores/useConfigStore.test.ts index e587c703..ca4562c0 100644 --- a/packages/ui/src/stores/useConfigStore.test.ts +++ b/packages/ui/src/stores/useConfigStore.test.ts @@ -408,6 +408,31 @@ describe('useConfigStore provider persistence', () => { expect(useConfigStore.getState().selectedProviderId).toBe('__add_provider__'); }); + test('add-provider sentinel is not persisted as a stable provider selection', async () => { + useConfigStore.setState({ + activeDirectoryKey: DIRECTORY, + currentProviderId: 'live', + currentModelId: 'live-model', + selectedProviderId: '__add_provider__', + directoryScoped: { + [DIRECTORY]: { + providers: [provider('live')], + agents: [], + currentProviderId: 'live', + currentModelId: 'live-model', + currentAgentName: undefined, + selectedProviderId: '__add_provider__', + agentModelSelections: {}, + defaultProviders: { default: 'live' }, + }, + }, + }); + + const persisted = JSON.parse(storage.get(STORAGE_KEY) ?? '{}'); + expect(persisted.state.selectedProviderId).toBe(''); + expect(persisted.state.directoryScoped[DIRECTORY].selectedProviderId).toBe(''); + }); + test('setAgent applies settings default variant for an agent configured model', () => { useSessionUIStore.setState({ currentSessionId: 'ses_agent_default_variant' }); useConfigStore.setState({ @@ -603,6 +628,47 @@ describe('useConfigStore provider persistence', () => { expect(state.currentAgentName).toBe('review'); }); + test('sync config defaults do not close the add-provider settings flow', () => { + useConfigStore.setState({ + activeDirectoryKey: DIRECTORY, + providers: [provider('openai', 'gpt-5.5'), provider('anthropic', 'claude')], + agents: [ + testAgent('build', { model: { providerID: 'anthropic', modelID: 'claude' } }), + testAgent('review', { model: { providerID: 'openai', modelID: 'gpt-5.5' } }), + ], + currentProviderId: 'anthropic', + currentModelId: 'claude', + currentAgentName: 'build', + selectedProviderId: '__add_provider__', + selectionSource: 'auto', + directoryScoped: { + [DIRECTORY]: { + providers: [provider('openai', 'gpt-5.5'), provider('anthropic', 'claude')], + agents: [ + testAgent('build', { model: { providerID: 'anthropic', modelID: 'claude' } }), + testAgent('review', { model: { providerID: 'openai', modelID: 'gpt-5.5' } }), + ], + currentProviderId: 'anthropic', + currentModelId: 'claude', + currentAgentName: 'build', + selectedProviderId: '__add_provider__', + agentModelSelections: {}, + defaultProviders: {}, + selectionSource: 'auto', + }, + }, + }); + + emitSyncConfigChanged(DIRECTORY, { default_agent: 'review', model: 'openai/gpt-5.5' }); + + const state = useConfigStore.getState(); + expect(state.currentAgentName).toBe('review'); + expect(state.currentProviderId).toBe('openai'); + expect(state.currentModelId).toBe('gpt-5.5'); + expect(state.selectedProviderId).toBe('__add_provider__'); + expect(state.directoryScoped[DIRECTORY]?.selectedProviderId).toBe('__add_provider__'); + }); + test('duplicate sync config event is a no-op when defaults and selection are unchanged', () => { useConfigStore.setState({ activeDirectoryKey: DIRECTORY, diff --git a/packages/ui/src/stores/useConfigStore.ts b/packages/ui/src/stores/useConfigStore.ts index 2563b092..ae0f3131 100644 --- a/packages/ui/src/stores/useConfigStore.ts +++ b/packages/ui/src/stores/useConfigStore.ts @@ -31,7 +31,8 @@ const STT_SILENCE_HOLD_MS_MAX = 10000; const FALLBACK_PROVIDER_ID = "opencode"; const FALLBACK_MODEL_ID = "big-pickle"; // Sentinel selectedProviderId used by the providers UI while the "Add provider" -// form is open. It is intentionally not a real provider id. +// form is open. It is intentionally not a real provider id and must not be +// persisted as a stable provider selection. const ADD_PROVIDER_SENTINEL = "__add_provider__"; const GIT_UTILITY_PROVIDER_ID = "zen"; const GIT_UTILITY_PREFERRED_MODEL_ID = "big-pickle"; @@ -189,6 +190,14 @@ type ProviderWithModelList = Omit & { models: ProviderModel[ type GitModelSelection = { providerId: string; modelId: string }; type ProviderModelSelection = { providerId: string; modelId: string; variant?: string } | null; +const sanitizePersistedSelectedProviderId = (providerId: string | undefined): string => ( + providerId === ADD_PROVIDER_SENTINEL ? "" : (providerId ?? "") +); + +const preserveAddProviderSelection = (currentSelectedProviderId: string | undefined, nextProviderId: string): string => ( + currentSelectedProviderId === ADD_PROVIDER_SENTINEL ? ADD_PROVIDER_SENTINEL : nextProviderId +); + const normalizeOptionalString = (value: unknown): string | undefined => { if (typeof value !== "string") { return undefined; @@ -2424,7 +2433,7 @@ export const useConfigStore = create()( currentProviderId: providerId, currentModelId: modelId, currentVariant: variant, - selectedProviderId: providerId, + selectedProviderId: preserveAddProviderSelection(state.selectedProviderId, providerId), selectionSource: "manual", }; @@ -2432,7 +2441,7 @@ export const useConfigStore = create()( currentProviderId: providerId, currentModelId: modelId, currentVariant: variant, - selectedProviderId: providerId, + selectedProviderId: preserveAddProviderSelection(state.selectedProviderId, providerId), selectionSource: "manual", directoryScoped: { ...state.directoryScoped, @@ -2577,7 +2586,7 @@ export const useConfigStore = create()( currentProviderId: resolvedProviderId, currentModelId: resolvedModelId, currentVariant: resolvedVariant, - selectedProviderId: resolvedProviderId, + selectedProviderId: preserveAddProviderSelection(state.selectedProviderId, resolvedProviderId), } : {}), selectionSource: "auto", @@ -2596,7 +2605,7 @@ export const useConfigStore = create()( nextState.currentProviderId = resolvedProviderId; nextState.currentModelId = resolvedModelId; nextState.currentVariant = resolvedVariant; - nextState.selectedProviderId = resolvedProviderId; + nextState.selectedProviderId = preserveAddProviderSelection(state.selectedProviderId, resolvedProviderId); } return nextState; @@ -2678,6 +2687,7 @@ export const useConfigStore = create()( const currentProviderId = isActive ? state.currentProviderId : baseSnapshot.currentProviderId; const currentModelId = isActive ? state.currentModelId : baseSnapshot.currentModelId; const currentVariant = isActive ? state.currentVariant : baseSnapshot.currentVariant; + const currentSelectedProviderId = isActive ? state.selectedProviderId : baseSnapshot.selectedProviderId; const nextSelection = resolveSelectionWithManualGuard({ agents, providers, @@ -2702,7 +2712,7 @@ export const useConfigStore = create()( currentProviderId: nextSelection.providerId, currentModelId: nextSelection.modelId, currentVariant: nextSelection.variant, - selectedProviderId: nextSelection.providerId, + selectedProviderId: preserveAddProviderSelection(currentSelectedProviderId, nextSelection.providerId), } : {}), selectionSource: nextSelection.selectionSource, @@ -2721,7 +2731,7 @@ export const useConfigStore = create()( state.currentProviderId !== nextSelection.providerId || state.currentModelId !== nextSelection.modelId || state.currentVariant !== nextSelection.variant - || state.selectedProviderId !== nextSelection.providerId + || state.selectedProviderId !== preserveAddProviderSelection(currentSelectedProviderId, nextSelection.providerId) )) )); @@ -2741,7 +2751,7 @@ export const useConfigStore = create()( nextState.currentProviderId = nextSelection.providerId; nextState.currentModelId = nextSelection.modelId; nextState.currentVariant = nextSelection.variant; - nextState.selectedProviderId = nextSelection.providerId; + nextState.selectedProviderId = preserveAddProviderSelection(currentSelectedProviderId, nextSelection.providerId); } } @@ -3288,14 +3298,22 @@ export const useConfigStore = create()( // success) and by the provider/agent config-change subscriptions. partialize: (state) => ({ activeDirectoryKey: state.activeDirectoryKey, - directoryScoped: state.directoryScoped, + directoryScoped: Object.fromEntries( + Object.entries(state.directoryScoped).map(([directoryKey, snapshot]) => [ + directoryKey, + { + ...snapshot, + selectedProviderId: sanitizePersistedSelectedProviderId(snapshot.selectedProviderId), + }, + ]), + ), providers: state.providers, agents: state.agents, currentProviderId: state.currentProviderId, currentModelId: state.currentModelId, currentVariant: state.currentVariant, currentAgentName: state.currentAgentName, - selectedProviderId: state.selectedProviderId, + selectedProviderId: sanitizePersistedSelectedProviderId(state.selectedProviderId), agentModelSelections: state.agentModelSelections, defaultProviders: state.defaultProviders, settingsDefaultModel: state.settingsDefaultModel,