From 20120d111bf37689c2de49f30f641493ad4cb65b Mon Sep 17 00:00:00 2001 From: Matt Cowger Date: Sat, 7 Nov 2026 13:27:25 -0800 Subject: [PATCH] fix(app): select available host model by default --- docs/forms.md | 3 ++ .../resolve-agent-form.test.ts | 37 ++++++++++++++++--- .../provider-selection/resolve-agent-form.ts | 5 ++- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/docs/forms.md b/docs/forms.md index 073dde368f..f966be00ba 100644 --- a/docs/forms.md +++ b/docs/forms.md @@ -47,6 +47,9 @@ The component renders state and dispatches intent. That is all it does. 4. **Resolution is explicit model state, per host** (`idle | pending | complete`), keyed off the opened snapshot's serverId. Waiting for data is a state you can render, not an effect race. + Create-agent model preferences are shared across hosts, so resolve remembered + models against the selected host's ready catalog before applying them. Keep + explicit initial values and user selections intact. 5. **Displays are owned state.** The selected option's label is captured at selection/seed time (`setProject(value, display)`), never re-derived from a live options list — list churn must not flicker or blank a selection. 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 3912d87e13..06f77b4b81 100644 --- a/packages/app/src/provider-selection/resolve-agent-form.test.ts +++ b/packages/app/src/provider-selection/resolve-agent-form.test.ts @@ -131,6 +131,34 @@ describe("resolveDefaultModel", () => { }); }); +describe("remembered model availability", () => { + it("uses the selected host's default when the remembered model belongs to another host", () => { + const preferences = { + provider: "codex" as const, + providerPreferences: { codex: { model: "remote/model" } }, + }; + const models: AgentModelDefinition[] = [ + { provider: "codex", id: "local/first", label: "First" }, + { provider: "codex", id: "local/default", label: "Default", isDefault: true }, + ]; + const resolve = (availableModels: AgentModelDefinition[] | null) => + resolveFormState( + undefined, + preferences, + availableModels, + INITIAL_USER_MODIFIED, + makeState().form, + codexProviderMap, + ).model; + + expect(resolve(null)).toBe("remote/model"); + expect(resolve(models)).toBe("local/default"); + expect(resolve([{ provider: "codex", id: "remote/model", label: "Remote" }])).toBe( + "remote/model", + ); + }); +}); + describe("model aliases", () => { it("canonicalizes a retired preferred model and restores thinking from its alias key", () => { const resolved = resolveFormState( @@ -791,17 +819,16 @@ describe("resolveFormState", () => { }); }); -it("keeps the explicit model when a refreshed catalogue no longer lists it", () => { +it("keeps a user-selected model when a refreshed catalog no longer lists it", () => { const resolved = resolveFormState( undefined, - { provider: "codex", providerPreferences: { codex: { model: "gpt-6-astra" } } }, + { provider: "codex" }, CODEX_MODELS, - INITIAL_USER_MODIFIED, - makeState().form, + { ...INITIAL_USER_MODIFIED, model: true }, + makeState({ provider: "codex", model: "gpt-6-astra" }).form, codexProviderMap, ); expect(resolved.model).toBe("gpt-6-astra"); - // Label/persistence lookup must not reinterpret the submitted ID as another model. expect(resolveEffectiveModel(CODEX_MODELS, resolved.model)).toBeNull(); }); diff --git a/packages/app/src/provider-selection/resolve-agent-form.ts b/packages/app/src/provider-selection/resolve-agent-form.ts index 08e88fd7a3..5f8d9f46c3 100644 --- a/packages/app/src/provider-selection/resolve-agent-form.ts +++ b/packages/app/src/provider-selection/resolve-agent-form.ts @@ -317,9 +317,10 @@ function resolveModelField(input: { : resolveCanonicalModelId(availableModels, initialModel) || initialModel; } if (preferredModel) { - return !availableModels + return !availableModels?.length ? preferredModel - : resolveCanonicalModelId(availableModels, preferredModel) || preferredModel; + : resolveCanonicalModelId(availableModels, preferredModel) || + resolveDefaultModelId(availableModels); } return ""; }