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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user