From 9e0c9e85375efc0566a32eff3cf8e45310c7070a Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Sat, 7 Nov 2026 22:51:32 +0000 Subject: [PATCH] fix(app): preserve pending form edits without repeated updates Ignore equivalent pending inputs so freshly built fork defaults do not keep the view rendering. Apply fork defaults only to their provider and model, preserving mode and thinking choices made while discovery is pending. --- .../resolve-agent-form.test.ts | 106 ++++++++++++++++++ .../provider-selection/resolve-agent-form.ts | 29 +++-- 2 files changed, 123 insertions(+), 12 deletions(-) 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) {