diff --git a/packages/ui/src/stores/useConfigStore.test.ts b/packages/ui/src/stores/useConfigStore.test.ts index 52dbe3e6..ec8fd62a 100644 --- a/packages/ui/src/stores/useConfigStore.test.ts +++ b/packages/ui/src/stores/useConfigStore.test.ts @@ -405,6 +405,60 @@ describe('useConfigStore provider persistence', () => { expect(state.currentVariant).toBe('fast'); }); + test('the settings provider selection survives a refresh that no longer lists it', async () => { + // Plugin-registered providers vanish from the list while OpenCode restarts. + // A refresh in that window used to move the user to another provider while + // they were reading or editing the one they picked. + useConfigStore.setState({ + activeDirectoryKey: DIRECTORY, + currentProviderId: 'live', + currentModelId: 'live-model', + selectedProviderId: 'plugin-provider', + directoryScoped: {}, + }); + + liveProviderId = 'live'; + await useConfigStore.getState().loadProviders({ source: 'test:missing-selection' }); + + const state = useConfigStore.getState(); + expect(state.providers.map((entry) => entry.id)).toEqual(['live']); + expect(state.selectedProviderId).toBe('plugin-provider'); + expect(state.directoryScoped[DIRECTORY]?.selectedProviderId).toBe('plugin-provider'); + }); + + test('an empty settings provider selection is filled from the refreshed list', async () => { + useConfigStore.setState({ + activeDirectoryKey: DIRECTORY, + currentProviderId: '', + currentModelId: '', + selectedProviderId: '', + directoryScoped: {}, + }); + + liveProviderId = 'live'; + await useConfigStore.getState().loadProviders({ source: 'test:empty-selection' }); + + expect(useConfigStore.getState().selectedProviderId).toBe('live'); + }); + + test('changing the chat provider leaves the settings provider selection alone', async () => { + useConfigStore.setState({ + activeDirectoryKey: DIRECTORY, + providers: [provider('anthropic'), provider('openai')], + currentProviderId: 'anthropic', + currentModelId: 'anthropic-model', + selectedProviderId: 'openai', + directoryScoped: {}, + }); + + useConfigStore.getState().setProvider('anthropic'); + + const state = useConfigStore.getState(); + expect(state.currentProviderId).toBe('anthropic'); + expect(state.selectedProviderId).toBe('openai'); + expect(state.directoryScoped[DIRECTORY]?.selectedProviderId).toBe('openai'); + }); + test('provider reload preserves the add-provider sentinel selection', async () => { // The user has opened the "Add provider" form, which sets selectedProviderId // to the sentinel. A background provider refresh must not navigate them away diff --git a/packages/ui/src/stores/useConfigStore.ts b/packages/ui/src/stores/useConfigStore.ts index 4a7dfd2e..0ab496df 100644 --- a/packages/ui/src/stores/useConfigStore.ts +++ b/packages/ui/src/stores/useConfigStore.ts @@ -187,10 +187,6 @@ const sanitizePersistedSelectedProviderId = (providerId: string | undefined): st 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; @@ -1609,10 +1605,13 @@ export const useConfigStore = create()( const currentSelectedProviderId = state.activeDirectoryKey === directoryKey ? state.selectedProviderId : baseSnapshot.selectedProviderId; - // Preserve the add-provider sentinel so a background refresh does not - // navigate the user out of the in-progress add-provider form (issue #1765). - const selectedProviderId = currentSelectedProviderId === ADD_PROVIDER_SENTINEL - || processedProviders.some((provider) => provider.id === currentSelectedProviderId) + // The Providers settings selection belongs to the user, not to this + // loader. A refresh may report a different provider set — an OpenCode + // restart drops plugin-registered providers until they re-register — + // and re-deriving a selection here yanked the open provider away + // mid-edit. Keep whatever is selected; only fill in an empty one. + // The add-provider sentinel is kept for the same reason (issue #1765). + const selectedProviderId = currentSelectedProviderId ? currentSelectedProviderId : (resolvedModel?.providerId ?? processedProviders[0]?.id ?? ""); @@ -1723,12 +1722,17 @@ export const useConfigStore = create()( nextState.currentProviderId = parsed.providerId; nextState.currentModelId = parsed.modelId; nextState.currentVariant = currentVariant; - nextState.selectedProviderId = parsed.providerId; nextSnapshot.currentProviderId = parsed.providerId; nextSnapshot.currentModelId = parsed.modelId; nextSnapshot.currentVariant = currentVariant; - nextSnapshot.selectedProviderId = parsed.providerId; + + // Only adopt this as the settings selection when the user has + // none; a failed refresh must not move an existing one. + if (!state.selectedProviderId) { + nextState.selectedProviderId = parsed.providerId; + nextSnapshot.selectedProviderId = parsed.providerId; + } } } } @@ -1771,14 +1775,12 @@ export const useConfigStore = create()( ...baseSnapshot, currentProviderId: providerId, currentModelId: newModelId, - selectedProviderId: providerId, selectionSource: "manual", }; return { currentProviderId: providerId, currentModelId: newModelId, - selectedProviderId: providerId, selectionSource: "manual", directoryScoped: { ...state.directoryScoped, @@ -2456,7 +2458,6 @@ export const useConfigStore = create()( currentProviderId: providerId, currentModelId: modelId, currentVariant: variant, - selectedProviderId: preserveAddProviderSelection(state.selectedProviderId, providerId), selectionSource: "manual", }; @@ -2464,7 +2465,6 @@ export const useConfigStore = create()( currentProviderId: providerId, currentModelId: modelId, currentVariant: variant, - selectedProviderId: preserveAddProviderSelection(state.selectedProviderId, providerId), selectionSource: "manual", directoryScoped: { ...state.directoryScoped, @@ -2616,7 +2616,6 @@ export const useConfigStore = create()( currentProviderId: resolvedProviderId, currentModelId: resolvedModelId, currentVariant: resolvedVariant, - selectedProviderId: preserveAddProviderSelection(state.selectedProviderId, resolvedProviderId), } : {}), selectionSource: "auto", @@ -2635,7 +2634,6 @@ export const useConfigStore = create()( nextState.currentProviderId = resolvedProviderId; nextState.currentModelId = resolvedModelId; nextState.currentVariant = resolvedVariant; - nextState.selectedProviderId = preserveAddProviderSelection(state.selectedProviderId, resolvedProviderId); } return nextState; @@ -2717,7 +2715,6 @@ 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, @@ -2742,7 +2739,6 @@ export const useConfigStore = create()( currentProviderId: nextSelection.providerId, currentModelId: nextSelection.modelId, currentVariant: nextSelection.variant, - selectedProviderId: preserveAddProviderSelection(currentSelectedProviderId, nextSelection.providerId), } : {}), selectionSource: nextSelection.selectionSource, @@ -2761,7 +2757,6 @@ export const useConfigStore = create()( state.currentProviderId !== nextSelection.providerId || state.currentModelId !== nextSelection.modelId || state.currentVariant !== nextSelection.variant - || state.selectedProviderId !== preserveAddProviderSelection(currentSelectedProviderId, nextSelection.providerId) )) )); @@ -2781,7 +2776,6 @@ export const useConfigStore = create()( nextState.currentProviderId = nextSelection.providerId; nextState.currentModelId = nextSelection.modelId; nextState.currentVariant = nextSelection.variant; - nextState.selectedProviderId = preserveAddProviderSelection(currentSelectedProviderId, nextSelection.providerId); } }