From 009d46600e373766e52ce1d65509250b750082a3 Mon Sep 17 00:00:00 2001 From: Rohit C Prasad Date: Tue, 21 Jul 2026 16:05:15 -0700 Subject: [PATCH] composer: drop the hardcoded model fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Until /v1/settings supplies the list the picker is a disabled "Loading models…" chip. The baked-in list had gone stale and offered phantom ids during the boot race. --- .../gui/e2e/composer-model-loading.spec.ts | 18 ++++++++++++ surfaces/gui/src/components/Composer.tsx | 28 +++++++++++++------ 2 files changed, 38 insertions(+), 8 deletions(-) create mode 100644 surfaces/gui/e2e/composer-model-loading.spec.ts diff --git a/surfaces/gui/e2e/composer-model-loading.spec.ts b/surfaces/gui/e2e/composer-model-loading.spec.ts new file mode 100644 index 00000000..e4f7deb6 --- /dev/null +++ b/surfaces/gui/e2e/composer-model-loading.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from "./fixtures"; + +// The composer must never advertise models the backend didn't confirm: before the +// /v1/settings list arrives (cold app boot races the sidecar), the picker is a +// disabled "Loading models…" chip — NOT a hardcoded fallback list, which went stale +// and offered phantom ids (caught by owner, 2026-07-21). +test("picker shows a disabled Loading-models chip until the list arrives", async ({ page }) => { + await page.route("**/v1/settings", (r) => + r.fulfill({ + json: { model: "gpt-5.5", models: [], model_labels: {}, has_key: true, model_ready: true, onboarded: true, nav_layout: "flat" }, + }), + ); + await page.goto("/"); + const chip = page.getByTestId("models-loading"); + await expect(chip).toBeVisible(); + await expect(chip).toBeDisabled(); + await expect(chip).toContainText("Loading models…"); +}); diff --git a/surfaces/gui/src/components/Composer.tsx b/surfaces/gui/src/components/Composer.tsx index 19463a39..011a8e19 100644 --- a/surfaces/gui/src/components/Composer.tsx +++ b/surfaces/gui/src/components/Composer.tsx @@ -23,9 +23,9 @@ const PERMISSION_OPTIONS: Option[] = [ { value: "custom", label: "Custom", description: "Use auto-allow rules from config.toml" }, ]; -// Fallback list when the server hasn't supplied one yet; the live list (incl. detected Ollama -// models) arrives via the `models` prop. -const MODEL_VALUES = ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5"]; +// No hardcoded model fallback: until the server supplies the list (a few seconds after a +// cold app boot), the picker renders a disabled "Loading models…" chip. A baked-in list +// goes stale and silently offers ids the backend never confirmed (caught 2026-07-21). // Drop the provider prefix for display (anthropic:claude-opus-4-8 → claude-opus-4-8); full id on hover. const shortModel = (m: string) => (m.includes(":") ? m.split(":").slice(1).join(":") : m); @@ -319,8 +319,10 @@ export function Composer(props: Props) { } }; - const available = props.models && props.models.length ? props.models : MODEL_VALUES; - const modelOptions: Option[] = Array.from(new Set([props.model, ...available])).map((m) => ({ + const modelsLoaded = !!(props.models && props.models.length); + const modelOptions: Option[] = Array.from( + new Set([props.model, ...(props.models || [])]), + ).map((m) => ({ value: m, label: props.modelLabels?.[m] || shortModel(m), })); @@ -472,9 +474,19 @@ export function Composer(props: Props) { ) : ( - !props.modelLocked && ( - - ) + !props.modelLocked && + (modelsLoaded ? ( + + ) : ( + + )) ))} {/* mic — immediately before send (owner call, DMG #28 walkthrough) */}