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');
});
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
+14 -20
View File
@@ -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<ConfigStore>()(
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<ConfigStore>()(
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<ConfigStore>()(
...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<ConfigStore>()(
currentProviderId: providerId,
currentModelId: modelId,
currentVariant: variant,
selectedProviderId: preserveAddProviderSelection(state.selectedProviderId, providerId),
selectionSource: "manual",
};
@@ -2464,7 +2465,6 @@ export const useConfigStore = create<ConfigStore>()(
currentProviderId: providerId,
currentModelId: modelId,
currentVariant: variant,
selectedProviderId: preserveAddProviderSelection(state.selectedProviderId, providerId),
selectionSource: "manual",
directoryScoped: {
...state.directoryScoped,
@@ -2616,7 +2616,6 @@ export const useConfigStore = create<ConfigStore>()(
currentProviderId: resolvedProviderId,
currentModelId: resolvedModelId,
currentVariant: resolvedVariant,
selectedProviderId: preserveAddProviderSelection(state.selectedProviderId, resolvedProviderId),
}
: {}),
selectionSource: "auto",
@@ -2635,7 +2634,6 @@ export const useConfigStore = create<ConfigStore>()(
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<ConfigStore>()(
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<ConfigStore>()(
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<ConfigStore>()(
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<ConfigStore>()(
nextState.currentProviderId = nextSelection.providerId;
nextState.currentModelId = nextSelection.modelId;
nextState.currentVariant = nextSelection.variant;
nextState.selectedProviderId = preserveAddProviderSelection(currentSelectedProviderId, nextSelection.providerId);
}
}