fix(providers): keep the settings provider selection under one owner

The selected provider on the Providers settings page shared a field with
the chat's model resolution. Changing the chat model or agent, and
`loadAgents` reconciling a selection, all wrote to it, so the page jumped
to an unrelated provider while the user was reading or editing one.

Only the sidebar click writes it now. A refresh no longer re-derives it
either: a provider missing from a refreshed list is expected while
OpenCode restarts and plugin-registered providers re-register.
This commit is contained in:
Bohdan Triapitsyn
2026-08-22 20:31:31 +03:00
parent c452a8092c
commit 887127b5d0
2 changed files with 68 additions and 20 deletions
@@ -405,6 +405,60 @@ describe('useConfigStore provider persistence', () => {
expect(state.currentVariant).toBe('fast'); 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 () => { test('provider reload preserves the add-provider sentinel selection', async () => {
// The user has opened the "Add provider" form, which sets selectedProviderId // The user has opened the "Add provider" form, which sets selectedProviderId
// to the sentinel. A background provider refresh must not navigate them away // to the sentinel. A background provider refresh must not navigate them away
+14 -20
View File
@@ -187,10 +187,6 @@ const sanitizePersistedSelectedProviderId = (providerId: string | undefined): st
providerId === ADD_PROVIDER_SENTINEL ? "" : (providerId ?? "") 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 => { const normalizeOptionalString = (value: unknown): string | undefined => {
if (typeof value !== "string") { if (typeof value !== "string") {
return undefined; return undefined;
@@ -1609,10 +1605,13 @@ export const useConfigStore = create<ConfigStore>()(
const currentSelectedProviderId = state.activeDirectoryKey === directoryKey const currentSelectedProviderId = state.activeDirectoryKey === directoryKey
? state.selectedProviderId ? state.selectedProviderId
: baseSnapshot.selectedProviderId; : baseSnapshot.selectedProviderId;
// Preserve the add-provider sentinel so a background refresh does not // The Providers settings selection belongs to the user, not to this
// navigate the user out of the in-progress add-provider form (issue #1765). // loader. A refresh may report a different provider set — an OpenCode
const selectedProviderId = currentSelectedProviderId === ADD_PROVIDER_SENTINEL // restart drops plugin-registered providers until they re-register —
|| processedProviders.some((provider) => provider.id === currentSelectedProviderId) // 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 ? currentSelectedProviderId
: (resolvedModel?.providerId ?? processedProviders[0]?.id ?? ""); : (resolvedModel?.providerId ?? processedProviders[0]?.id ?? "");
@@ -1723,12 +1722,17 @@ export const useConfigStore = create<ConfigStore>()(
nextState.currentProviderId = parsed.providerId; nextState.currentProviderId = parsed.providerId;
nextState.currentModelId = parsed.modelId; nextState.currentModelId = parsed.modelId;
nextState.currentVariant = currentVariant; nextState.currentVariant = currentVariant;
nextState.selectedProviderId = parsed.providerId;
nextSnapshot.currentProviderId = parsed.providerId; nextSnapshot.currentProviderId = parsed.providerId;
nextSnapshot.currentModelId = parsed.modelId; nextSnapshot.currentModelId = parsed.modelId;
nextSnapshot.currentVariant = currentVariant; 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<ConfigStore>()(
...baseSnapshot, ...baseSnapshot,
currentProviderId: providerId, currentProviderId: providerId,
currentModelId: newModelId, currentModelId: newModelId,
selectedProviderId: providerId,
selectionSource: "manual", selectionSource: "manual",
}; };
return { return {
currentProviderId: providerId, currentProviderId: providerId,
currentModelId: newModelId, currentModelId: newModelId,
selectedProviderId: providerId,
selectionSource: "manual", selectionSource: "manual",
directoryScoped: { directoryScoped: {
...state.directoryScoped, ...state.directoryScoped,
@@ -2456,7 +2458,6 @@ export const useConfigStore = create<ConfigStore>()(
currentProviderId: providerId, currentProviderId: providerId,
currentModelId: modelId, currentModelId: modelId,
currentVariant: variant, currentVariant: variant,
selectedProviderId: preserveAddProviderSelection(state.selectedProviderId, providerId),
selectionSource: "manual", selectionSource: "manual",
}; };
@@ -2464,7 +2465,6 @@ export const useConfigStore = create<ConfigStore>()(
currentProviderId: providerId, currentProviderId: providerId,
currentModelId: modelId, currentModelId: modelId,
currentVariant: variant, currentVariant: variant,
selectedProviderId: preserveAddProviderSelection(state.selectedProviderId, providerId),
selectionSource: "manual", selectionSource: "manual",
directoryScoped: { directoryScoped: {
...state.directoryScoped, ...state.directoryScoped,
@@ -2616,7 +2616,6 @@ export const useConfigStore = create<ConfigStore>()(
currentProviderId: resolvedProviderId, currentProviderId: resolvedProviderId,
currentModelId: resolvedModelId, currentModelId: resolvedModelId,
currentVariant: resolvedVariant, currentVariant: resolvedVariant,
selectedProviderId: preserveAddProviderSelection(state.selectedProviderId, resolvedProviderId),
} }
: {}), : {}),
selectionSource: "auto", selectionSource: "auto",
@@ -2635,7 +2634,6 @@ export const useConfigStore = create<ConfigStore>()(
nextState.currentProviderId = resolvedProviderId; nextState.currentProviderId = resolvedProviderId;
nextState.currentModelId = resolvedModelId; nextState.currentModelId = resolvedModelId;
nextState.currentVariant = resolvedVariant; nextState.currentVariant = resolvedVariant;
nextState.selectedProviderId = preserveAddProviderSelection(state.selectedProviderId, resolvedProviderId);
} }
return nextState; return nextState;
@@ -2717,7 +2715,6 @@ export const useConfigStore = create<ConfigStore>()(
const currentProviderId = isActive ? state.currentProviderId : baseSnapshot.currentProviderId; const currentProviderId = isActive ? state.currentProviderId : baseSnapshot.currentProviderId;
const currentModelId = isActive ? state.currentModelId : baseSnapshot.currentModelId; const currentModelId = isActive ? state.currentModelId : baseSnapshot.currentModelId;
const currentVariant = isActive ? state.currentVariant : baseSnapshot.currentVariant; const currentVariant = isActive ? state.currentVariant : baseSnapshot.currentVariant;
const currentSelectedProviderId = isActive ? state.selectedProviderId : baseSnapshot.selectedProviderId;
const nextSelection = resolveSelectionWithManualGuard({ const nextSelection = resolveSelectionWithManualGuard({
agents, agents,
providers, providers,
@@ -2742,7 +2739,6 @@ export const useConfigStore = create<ConfigStore>()(
currentProviderId: nextSelection.providerId, currentProviderId: nextSelection.providerId,
currentModelId: nextSelection.modelId, currentModelId: nextSelection.modelId,
currentVariant: nextSelection.variant, currentVariant: nextSelection.variant,
selectedProviderId: preserveAddProviderSelection(currentSelectedProviderId, nextSelection.providerId),
} }
: {}), : {}),
selectionSource: nextSelection.selectionSource, selectionSource: nextSelection.selectionSource,
@@ -2761,7 +2757,6 @@ export const useConfigStore = create<ConfigStore>()(
state.currentProviderId !== nextSelection.providerId state.currentProviderId !== nextSelection.providerId
|| state.currentModelId !== nextSelection.modelId || state.currentModelId !== nextSelection.modelId
|| state.currentVariant !== nextSelection.variant || state.currentVariant !== nextSelection.variant
|| state.selectedProviderId !== preserveAddProviderSelection(currentSelectedProviderId, nextSelection.providerId)
)) ))
)); ));
@@ -2781,7 +2776,6 @@ export const useConfigStore = create<ConfigStore>()(
nextState.currentProviderId = nextSelection.providerId; nextState.currentProviderId = nextSelection.providerId;
nextState.currentModelId = nextSelection.modelId; nextState.currentModelId = nextSelection.modelId;
nextState.currentVariant = nextSelection.variant; nextState.currentVariant = nextSelection.variant;
nextState.selectedProviderId = preserveAddProviderSelection(currentSelectedProviderId, nextSelection.providerId);
} }
} }