fix(app): resolve remembered models when host catalogs arrive

Keep opening model resolution pending until its selected catalog is ready, so a loading snapshot cannot freeze an unavailable preference. Preserve user edits and error presentation while awaiting recovery.

Co-authored-by: Matt Cowger <matt@cowger.us>
This commit is contained in:
paseo-bot[bot]
2026-11-07 22:25:32 +00:00
co-authored by Matt Cowger
parent 20120d111b
commit 4f7201665e
3 changed files with 87 additions and 2 deletions
@@ -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;
@@ -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");
});
@@ -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 };
}