diff --git a/packages/app/src/provider-selection/resolve-agent-form.test.ts b/packages/app/src/provider-selection/resolve-agent-form.test.ts index c4c4078c5f..aa6ca01048 100644 --- a/packages/app/src/provider-selection/resolve-agent-form.test.ts +++ b/packages/app/src/provider-selection/resolve-agent-form.test.ts @@ -1371,3 +1371,109 @@ it("resolves modeless providers once their empty catalog is ready", () => { expect(ready.form).toMatchObject({ provider: "pi", model: "" }); expect(ready.resolution.status).toBe("completed"); }); + +it("ignores equivalent pending inputs with freshly built fork defaults", () => { + const inputs = { + type: "INPUTS_CHANGED" as const, + serverId: "host", + isVisible: true, + isCreateFlow: true, + isPreferencesLoading: false, + hasSnapshot: true, + initialValues: { provider: "codex", model: "gpt-5.3-codex", modeId: "full-access" }, + preferences: null, + allowedProviderMap: codexProviderMap, + providerModelsByProvider: makeProviderModelsByProvider([["codex", null]]), + }; + const pending = resolveAgentForm(makeState(), inputs); + expect(resolveAgentForm(pending, { ...inputs, initialValues: { ...inputs.initialValues } })).toBe( + pending, + ); +}); + +it("keeps the new provider's mode and thinking when switching away from pending fork defaults", () => { + const claudeModels: AgentModelDefinition[] = [ + { + provider: "claude", + id: "sonnet", + label: "Sonnet", + defaultThinkingOptionId: "low", + thinkingOptions: [ + { id: "low", label: "Low" }, + { id: "high", label: "High" }, + ], + }, + ]; + const claudePrefs = { mode: "bypassPermissions", thinkingByModel: { sonnet: "high" } }; + const inputs = { + type: "INPUTS_CHANGED" as const, + serverId: "host", + isVisible: true, + isCreateFlow: true, + isPreferencesLoading: false, + hasSnapshot: true, + initialValues: { + provider: "codex", + model: "gpt-5.3-codex", + modeId: "full-access", + thinkingOptionId: "low", + }, + preferences: { providerPreferences: { claude: claudePrefs } }, + allowedProviderMap: bothProviderMap, + providerModelsByProvider: makeProviderModelsByProvider([ + ["codex", null], + ["claude", claudeModels], + ]), + }; + const pending = resolveAgentForm(makeState(), inputs); + const switched = resolveAgentForm(pending, { + type: "SET_PROVIDER_AND_MODEL_FROM_USER", + provider: "claude", + modelId: "sonnet", + providerDef: TEST_CLAUDE_DEFINITION, + providerModels: claudeModels, + providerPrefs: claudePrefs, + }); + expect(switched.form).toMatchObject({ + provider: "claude", + model: "sonnet", + modeId: "bypassPermissions", + thinkingOptionId: "high", + }); + expect(resolveAgentForm(switched, inputs).form).toEqual(switched.form); +}); + +it("keeps a new model's thinking preference when pending fork defaults finish resolving", () => { + const models: AgentModelDefinition[] = [ + ...CODEX_MODELS, + { ...CODEX_MODELS[0], id: "next-model", defaultThinkingOptionId: "low" }, + ]; + const preferences = { + providerPreferences: { codex: { thinkingByModel: { "next-model": "xhigh" } } }, + }; + const inputs = { + type: "INPUTS_CHANGED" as const, + serverId: "host", + isVisible: true, + isCreateFlow: true, + isPreferencesLoading: false, + hasSnapshot: true, + initialValues: { provider: "codex", model: "gpt-5.3-codex", thinkingOptionId: "low" }, + preferences, + allowedProviderMap: codexProviderMap, + providerModelsByProvider: makeProviderModelsByProvider([["codex", null]]), + }; + const pending = resolveAgentForm(makeState(), inputs); + const edited = resolveAgentForm(pending, { + type: "SET_MODEL_FROM_USER", + modelId: "next-model", + availableModels: models, + providerPrefs: preferences.providerPreferences.codex, + }); + expect(edited.form.thinkingOptionId).toBe("xhigh"); + const ready = resolveAgentForm(edited, { + ...inputs, + providerModelsByProvider: makeProviderModelsByProvider([["codex", models]]), + }); + expect(ready.form).toEqual(edited.form); +}); diff --git a/packages/app/src/provider-selection/resolve-agent-form.ts b/packages/app/src/provider-selection/resolve-agent-form.ts index f2de64bd6e..8db7c79d63 100644 --- a/packages/app/src/provider-selection/resolve-agent-form.ts +++ b/packages/app/src/provider-selection/resolve-agent-form.ts @@ -345,8 +345,11 @@ function resolveThinkingOption(input: { } = input; if (!provider) return ""; if (userModified) return currentThinkingOptionId; + const initialModel = normalizeSelectedModelId(initialValues?.model); + const matchesInitialModel = + !initialModel || resolveCanonicalModelId(availableModels, initialModel) === modelId; const initialThinkingOptionId = - typeof initialValues?.thinkingOptionId === "string" + matchesInitialModel && typeof initialValues?.thinkingOptionId === "string" ? initialValues.thinkingOptionId.trim() : ""; const preferredThinking = resolvePreferredThinkingOptionId({ @@ -376,6 +379,10 @@ export function resolveFormState( preferences, }); + const providerInitialValues = + initialValues?.provider && initialValues.provider !== result.provider + ? undefined + : initialValues; const providerDef = result.provider ? allowedProviderMap.get(result.provider) : undefined; const providerPrefs = result.provider ? preferences?.providerPreferences?.[result.provider] @@ -385,7 +392,7 @@ export function resolveFormState( provider: result.provider, userModified: userModified.modeId, currentModeId: result.modeId, - initialValues, + initialValues: providerInitialValues, providerDef, providerPrefs, }); @@ -394,7 +401,7 @@ export function resolveFormState( provider: result.provider, userModified: userModified.model, currentModel: result.model, - initialValues, + initialValues: providerInitialValues, providerPrefs, availableModels, }); @@ -404,7 +411,7 @@ export function resolveFormState( userModified: userModified.thinkingOptionId, currentThinkingOptionId: result.thinkingOptionId, modelId: result.model, - initialValues, + initialValues: providerInitialValues, providerPrefs, availableModels, }); @@ -535,14 +542,12 @@ function completeResolution( ); const waitingForModels = resolved.provider !== null && action.providerModelsByProvider.get(resolved.provider) === null; - const nextState = { - ...state, - resolution: waitingForModels - ? PENDING_AGENT_FORM_RESOLUTION - : ({ status: "completed" } as const), - }; - if (!hasFormStateChanged(state.form, resolved)) return nextState; - return { ...nextState, form: resolved }; + const resolution = waitingForModels + ? PENDING_AGENT_FORM_RESOLUTION + : ({ status: "completed" } as const); + const form = hasFormStateChanged(state.form, resolved) ? resolved : state.form; + if (form === state.form && resolution.status === state.resolution.status) return state; + return { ...state, resolution, form }; } function applyProfile(state: AgentFormReducerState, action: ApplyProfileAction) {