diff --git a/packages/app/src/hooks/use-agent-form-state.ts b/packages/app/src/hooks/use-agent-form-state.ts index 1522ce082e..dc241c2e34 100644 --- a/packages/app/src/hooks/use-agent-form-state.ts +++ b/packages/app/src/hooks/use-agent-form-state.ts @@ -113,7 +113,7 @@ function buildProviderModelsByProvider( for (const entry of snapshotEntries ?? []) { map.set( entry.provider, - entry.status === "ready" ? filterSelectableModels(entry.models ?? null) : null, + entry.status === "ready" ? filterSelectableModels(entry.models ?? []) : null, ); } return map; @@ -249,8 +249,11 @@ export function useAgentFormState(options: UseAgentFormStateOptions): UseAgentFo const modelSelectorProviders = snapshotModelSelectorProviders; const availableModels = snapshotSelectedProviderModels; const modeOptions = snapshotSelectedProviderModes; + const selectedProviderHasError = + snapshotSelectedEntry?.status === "error" || snapshotSelectedEntry?.status === "unavailable"; const isModelSelectionLoading = isConnected && + !selectedProviderHasError && (resolution.status === "pending" || snapshotIsLoading || selectedProviderIsLoading); const isAllModelsLoading = isModelSelectionLoading; 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 06f77b4b81..c4c4078c5f 100644 --- a/packages/app/src/provider-selection/resolve-agent-form.test.ts +++ b/packages/app/src/provider-selection/resolve-agent-form.test.ts @@ -1296,3 +1296,78 @@ it("owns input readiness, reopening and user edits in the reducer", () => { state = resolveAgentForm(state, { ...inputs, isPreferencesLoading: false, hasSnapshot: true }); expect(state.form).toMatchObject({ provider: "codex", model: "astra" }); }); + +it("resolves a remembered model against the new host after its catalog finishes loading", () => { + const inputs = { + type: "INPUTS_CHANGED" as const, + serverId: "second-host", + isVisible: true, + isCreateFlow: true, + isPreferencesLoading: false, + hasSnapshot: true, + initialValues: undefined, + preferences: { + provider: "codex", + providerPreferences: { codex: { model: "remote/model" } }, + }, + allowedProviderMap: codexProviderMap, + providerModelsByProvider: makeProviderModelsByProvider([["codex", null]]), + }; + const firstHost = resolveAgentForm(makeState(), { + ...inputs, + serverId: "first-host", + providerModelsByProvider: makeProviderModelsByProvider([ + ["codex", [{ provider: "codex", id: "remote/model", label: "Remote" }]], + ]), + }); + const opening = resolveAgentForm(firstHost, inputs); + expect(opening.form.model).toBe("remote/model"); + expect(opening.resolution.status).toBe("pending"); + const ready = resolveAgentForm(opening, { + ...inputs, + providerModelsByProvider: makeProviderModelsByProvider([["codex", CODEX_MODELS]]), + }); + expect(ready.form.model).toBe("gpt-5.3-codex"); + expect(ready.form.thinkingOptionId).toBe("xhigh"); + expect(ready.resolution.status).toBe("completed"); +}); + +it("retains user input while the opening catalog is pending", () => { + const inputs = { + type: "INPUTS_CHANGED" as const, + serverId: "host", + isVisible: true, + isCreateFlow: true, + isPreferencesLoading: false, + hasSnapshot: true, + initialValues: undefined, + preferences: { provider: "codex", providerPreferences: { codex: { model: "remote/model" } } }, + allowedProviderMap: codexProviderMap, + providerModelsByProvider: makeProviderModelsByProvider([["codex", null]]), + }; + const opening = resolveAgentForm(makeState(), inputs); + const edited = resolveAgentForm(opening, { + type: "SET_MODEL_FROM_USER", + modelId: "manual/model", + availableModels: null, + providerPrefs: undefined, + }); + const ready = resolveAgentForm(edited, { + ...inputs, + providerModelsByProvider: makeProviderModelsByProvider([["codex", CODEX_MODELS]]), + }); + expect(ready.form.model).toBe("manual/model"); + expect(ready.resolution.status).toBe("completed"); +}); + +it("resolves modeless providers once their empty catalog is ready", () => { + const ready = resolveAgentForm(makeState(), { + type: "COMPLETE_RESOLUTION", + initialValues: undefined, + preferences: { provider: "pi" }, + providerModelsByProvider: makeProviderModelsByProvider([["pi", []]]), + allowedProviderMap: buildProviderDefinitionMap([TEST_PI_DEFINITION]), + }); + expect(ready.form).toMatchObject({ provider: "pi", model: "" }); + expect(ready.resolution.status).toBe("completed"); +}); diff --git a/packages/app/src/provider-selection/resolve-agent-form.ts b/packages/app/src/provider-selection/resolve-agent-form.ts index 5f8d9f46c3..f2de64bd6e 100644 --- a/packages/app/src/provider-selection/resolve-agent-form.ts +++ b/packages/app/src/provider-selection/resolve-agent-form.ts @@ -533,7 +533,14 @@ function completeResolution( state.form, action.allowedProviderMap, ); - const nextState = { ...state, resolution: { status: "completed" } as const }; + 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 }; }