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