fix(providers): keep add-provider flow selected during sync

This commit is contained in:
Bohdan Triapitsyn
2026-06-28 13:05:00 +03:00
parent c5eab5ac1e
commit 1178c4e4f5
2 changed files with 94 additions and 10 deletions
@@ -408,6 +408,31 @@ describe('useConfigStore provider persistence', () => {
expect(useConfigStore.getState().selectedProviderId).toBe('__add_provider__'); expect(useConfigStore.getState().selectedProviderId).toBe('__add_provider__');
}); });
test('add-provider sentinel is not persisted as a stable provider selection', async () => {
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
currentProviderId: 'live',
currentModelId: 'live-model',
selectedProviderId: '__add_provider__',
directoryScoped: {
[DIRECTORY]: {
providers: [provider('live')],
agents: [],
currentProviderId: 'live',
currentModelId: 'live-model',
currentAgentName: undefined,
selectedProviderId: '__add_provider__',
agentModelSelections: {},
defaultProviders: { default: 'live' },
},
},
});
const persisted = JSON.parse(storage.get(STORAGE_KEY) ?? '{}');
expect(persisted.state.selectedProviderId).toBe('');
expect(persisted.state.directoryScoped[DIRECTORY].selectedProviderId).toBe('');
});
test('setAgent applies settings default variant for an agent configured model', () => { test('setAgent applies settings default variant for an agent configured model', () => {
useSessionUIStore.setState({ currentSessionId: 'ses_agent_default_variant' }); useSessionUIStore.setState({ currentSessionId: 'ses_agent_default_variant' });
useConfigStore.setState({ useConfigStore.setState({
@@ -603,6 +628,47 @@ describe('useConfigStore provider persistence', () => {
expect(state.currentAgentName).toBe('review'); expect(state.currentAgentName).toBe('review');
}); });
test('sync config defaults do not close the add-provider settings flow', () => {
useConfigStore.setState({
activeDirectoryKey: DIRECTORY,
providers: [provider('openai', 'gpt-5.5'), provider('anthropic', 'claude')],
agents: [
testAgent('build', { model: { providerID: 'anthropic', modelID: 'claude' } }),
testAgent('review', { model: { providerID: 'openai', modelID: 'gpt-5.5' } }),
],
currentProviderId: 'anthropic',
currentModelId: 'claude',
currentAgentName: 'build',
selectedProviderId: '__add_provider__',
selectionSource: 'auto',
directoryScoped: {
[DIRECTORY]: {
providers: [provider('openai', 'gpt-5.5'), provider('anthropic', 'claude')],
agents: [
testAgent('build', { model: { providerID: 'anthropic', modelID: 'claude' } }),
testAgent('review', { model: { providerID: 'openai', modelID: 'gpt-5.5' } }),
],
currentProviderId: 'anthropic',
currentModelId: 'claude',
currentAgentName: 'build',
selectedProviderId: '__add_provider__',
agentModelSelections: {},
defaultProviders: {},
selectionSource: 'auto',
},
},
});
emitSyncConfigChanged(DIRECTORY, { default_agent: 'review', model: 'openai/gpt-5.5' });
const state = useConfigStore.getState();
expect(state.currentAgentName).toBe('review');
expect(state.currentProviderId).toBe('openai');
expect(state.currentModelId).toBe('gpt-5.5');
expect(state.selectedProviderId).toBe('__add_provider__');
expect(state.directoryScoped[DIRECTORY]?.selectedProviderId).toBe('__add_provider__');
});
test('duplicate sync config event is a no-op when defaults and selection are unchanged', () => { test('duplicate sync config event is a no-op when defaults and selection are unchanged', () => {
useConfigStore.setState({ useConfigStore.setState({
activeDirectoryKey: DIRECTORY, activeDirectoryKey: DIRECTORY,
+28 -10
View File
@@ -31,7 +31,8 @@ const STT_SILENCE_HOLD_MS_MAX = 10000;
const FALLBACK_PROVIDER_ID = "opencode"; const FALLBACK_PROVIDER_ID = "opencode";
const FALLBACK_MODEL_ID = "big-pickle"; const FALLBACK_MODEL_ID = "big-pickle";
// Sentinel selectedProviderId used by the providers UI while the "Add provider" // Sentinel selectedProviderId used by the providers UI while the "Add provider"
// form is open. It is intentionally not a real provider id. // form is open. It is intentionally not a real provider id and must not be
// persisted as a stable provider selection.
const ADD_PROVIDER_SENTINEL = "__add_provider__"; const ADD_PROVIDER_SENTINEL = "__add_provider__";
const GIT_UTILITY_PROVIDER_ID = "zen"; const GIT_UTILITY_PROVIDER_ID = "zen";
const GIT_UTILITY_PREFERRED_MODEL_ID = "big-pickle"; const GIT_UTILITY_PREFERRED_MODEL_ID = "big-pickle";
@@ -189,6 +190,14 @@ type ProviderWithModelList = Omit<Provider, "models"> & { models: ProviderModel[
type GitModelSelection = { providerId: string; modelId: string }; type GitModelSelection = { providerId: string; modelId: string };
type ProviderModelSelection = { providerId: string; modelId: string; variant?: string } | null; type ProviderModelSelection = { providerId: string; modelId: string; variant?: string } | null;
const sanitizePersistedSelectedProviderId = (providerId: string | undefined): string => (
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;
@@ -2424,7 +2433,7 @@ export const useConfigStore = create<ConfigStore>()(
currentProviderId: providerId, currentProviderId: providerId,
currentModelId: modelId, currentModelId: modelId,
currentVariant: variant, currentVariant: variant,
selectedProviderId: providerId, selectedProviderId: preserveAddProviderSelection(state.selectedProviderId, providerId),
selectionSource: "manual", selectionSource: "manual",
}; };
@@ -2432,7 +2441,7 @@ export const useConfigStore = create<ConfigStore>()(
currentProviderId: providerId, currentProviderId: providerId,
currentModelId: modelId, currentModelId: modelId,
currentVariant: variant, currentVariant: variant,
selectedProviderId: providerId, selectedProviderId: preserveAddProviderSelection(state.selectedProviderId, providerId),
selectionSource: "manual", selectionSource: "manual",
directoryScoped: { directoryScoped: {
...state.directoryScoped, ...state.directoryScoped,
@@ -2577,7 +2586,7 @@ export const useConfigStore = create<ConfigStore>()(
currentProviderId: resolvedProviderId, currentProviderId: resolvedProviderId,
currentModelId: resolvedModelId, currentModelId: resolvedModelId,
currentVariant: resolvedVariant, currentVariant: resolvedVariant,
selectedProviderId: resolvedProviderId, selectedProviderId: preserveAddProviderSelection(state.selectedProviderId, resolvedProviderId),
} }
: {}), : {}),
selectionSource: "auto", selectionSource: "auto",
@@ -2596,7 +2605,7 @@ 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 = resolvedProviderId; nextState.selectedProviderId = preserveAddProviderSelection(state.selectedProviderId, resolvedProviderId);
} }
return nextState; return nextState;
@@ -2678,6 +2687,7 @@ 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,
@@ -2702,7 +2712,7 @@ export const useConfigStore = create<ConfigStore>()(
currentProviderId: nextSelection.providerId, currentProviderId: nextSelection.providerId,
currentModelId: nextSelection.modelId, currentModelId: nextSelection.modelId,
currentVariant: nextSelection.variant, currentVariant: nextSelection.variant,
selectedProviderId: nextSelection.providerId, selectedProviderId: preserveAddProviderSelection(currentSelectedProviderId, nextSelection.providerId),
} }
: {}), : {}),
selectionSource: nextSelection.selectionSource, selectionSource: nextSelection.selectionSource,
@@ -2721,7 +2731,7 @@ 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 !== nextSelection.providerId || state.selectedProviderId !== preserveAddProviderSelection(currentSelectedProviderId, nextSelection.providerId)
)) ))
)); ));
@@ -2741,7 +2751,7 @@ 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 = nextSelection.providerId; nextState.selectedProviderId = preserveAddProviderSelection(currentSelectedProviderId, nextSelection.providerId);
} }
} }
@@ -3288,14 +3298,22 @@ export const useConfigStore = create<ConfigStore>()(
// success) and by the provider/agent config-change subscriptions. // success) and by the provider/agent config-change subscriptions.
partialize: (state) => ({ partialize: (state) => ({
activeDirectoryKey: state.activeDirectoryKey, activeDirectoryKey: state.activeDirectoryKey,
directoryScoped: state.directoryScoped, directoryScoped: Object.fromEntries(
Object.entries(state.directoryScoped).map(([directoryKey, snapshot]) => [
directoryKey,
{
...snapshot,
selectedProviderId: sanitizePersistedSelectedProviderId(snapshot.selectedProviderId),
},
]),
),
providers: state.providers, providers: state.providers,
agents: state.agents, agents: state.agents,
currentProviderId: state.currentProviderId, currentProviderId: state.currentProviderId,
currentModelId: state.currentModelId, currentModelId: state.currentModelId,
currentVariant: state.currentVariant, currentVariant: state.currentVariant,
currentAgentName: state.currentAgentName, currentAgentName: state.currentAgentName,
selectedProviderId: state.selectedProviderId, selectedProviderId: sanitizePersistedSelectedProviderId(state.selectedProviderId),
agentModelSelections: state.agentModelSelections, agentModelSelections: state.agentModelSelections,
defaultProviders: state.defaultProviders, defaultProviders: state.defaultProviders,
settingsDefaultModel: state.settingsDefaultModel, settingsDefaultModel: state.settingsDefaultModel,