From 2eacd68b45aed247c475512aa2340865a12f2a0d Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 3 Sep 2026 00:25:42 -0700 Subject: [PATCH 1/3] Add tests for catalog error in the agent model picker A failed resolved-catalog load currently leaves the picker looking empty. These cases pin an inventory-style error, a disabled select, Retry plus a Settings hop, and copy that is not the empty-ready hint. --- packages/chat-ui/test/agents-section.test.tsx | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/packages/chat-ui/test/agents-section.test.tsx b/packages/chat-ui/test/agents-section.test.tsx index d3d1f90ed..7b1c96166 100644 --- a/packages/chat-ui/test/agents-section.test.tsx +++ b/packages/chat-ui/test/agents-section.test.tsx @@ -70,6 +70,10 @@ function stubFetch(options: { readonly catalogModels?: readonly (ModelFixture & { readonly offerings: readonly OfferingFixture[]; })[]; + /** When true, every catalog (`/models`) read fails. */ + readonly catalogFails?: boolean; + /** Fail the first N catalog reads, then succeed — used to exercise Retry. */ + readonly catalogFailUntil?: number; readonly addCapabilityFails?: boolean; readonly restoreFails?: boolean; readonly onSave?: ( @@ -100,6 +104,7 @@ function stubFetch(options: { offerings: [{ providerName: "anthropic" }], }, ]; + let catalogCalls = 0; globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { const path = typeof input === "string" ? input : String(input); @@ -127,6 +132,23 @@ function stubFetch(options: { return json({ settings: {}, contextWindow: 20 }); } if (/\/models$/.test(path)) { + catalogCalls += 1; + const catalogShouldFail = + options.catalogFails === true || + (options.catalogFailUntil !== undefined && + catalogCalls <= options.catalogFailUntil); + if (catalogShouldFail) { + return json( + { + error: { + code: "internal", + userMessage: "catalog boom", + refId: "ref_catalog", + }, + }, + 500, + ); + } return json( catalogModels.map((model, index) => ({ id: `model_${String(index)}`, @@ -859,6 +881,65 @@ describe("Agents section — Model select (CL-6272.3)", () => { expect(modelSelect(el)?.value).toBe(""); expect(modelSelect(el)?.options[0]?.textContent).toBe("No model set"); }); + + test("a catalog load failure reads as an error, not an empty picker (CL-6831)", async () => { + stubFetch({ catalogFails: true }); + const el = mount(baseProps()); + await settle(); + openAgent(el, "myra"); + await settle(); + + const select = modelSelect(el); + expect(select?.disabled).toBe(true); + + const alert = el.querySelector(".chat-dialog-error")?.textContent; + expect(alert).toBe("Couldn't load the models."); + expect(el.textContent).not.toContain( + "No connected providers yet — connect one in Shared Settings.", + ); + expect(el.textContent).not.toContain("No connected providers yet"); + + expect(findButton(el, "Retry")).toBeDefined(); + const settingsHop = el.querySelector( + 'a[href="/settings/connections"]', + ) as HTMLAnchorElement | null; + expect(settingsHop).not.toBeNull(); + expect(settingsHop?.textContent).toBe("Shared Settings"); + }); + + test("Retry after a catalog load failure recovers the picker", async () => { + stubFetch({ + catalogFailUntil: 1, + catalogModels: [ + { + canonicalName: "anthropic/claude-sonnet", + displayName: "Claude Sonnet", + offerings: [{ providerName: "anthropic" }], + }, + ], + }); + const el = mount(baseProps()); + await settle(); + openAgent(el, "myra"); + await settle(); + + expect(modelSelect(el)?.disabled).toBe(true); + expect(el.querySelector(".chat-dialog-error")?.textContent).toBe( + "Couldn't load the models.", + ); + + act(() => { + findButton(el, "Retry")?.click(); + }); + await settle(); + + expect(el.querySelector(".chat-dialog-error")).toBeNull(); + const select = modelSelect(el); + expect(select?.disabled).toBe(false); + expect( + Array.from(select?.options ?? []).map((option) => option.textContent), + ).toContain("Claude Sonnet · Anthropic"); + }); }); describe("Agents section — History", () => { From 562de549ae6f64a4d5dcfa35031c984c3634f343 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 3 Sep 2026 00:51:48 -0700 Subject: [PATCH 2/3] Show catalog load failure as an error, not an empty picker The resolved-catalog catch now keeps a message via describeChatError, disables the model select, and offers Retry plus a Shared Settings hop instead of looking like a connected-providers empty state. --- packages/chat-ui/src/strings.ts | 3 ++ .../src/workbench-settings/agents-section.tsx | 46 +++++++++++++++++-- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/packages/chat-ui/src/strings.ts b/packages/chat-ui/src/strings.ts index 4a9790311..4017e1361 100644 --- a/packages/chat-ui/src/strings.ts +++ b/packages/chat-ui/src/strings.ts @@ -432,6 +432,9 @@ export const CHAT_STRINGS = { ) => `${canonicalName} · ${providerName}`, workbenchSettingsAgentDetailNoConnectedModels: "No connected providers yet — connect one in Shared Settings.", + workbenchSettingsAgentDetailCatalogError: "Couldn't load the models.", + workbenchSettingsAgentDetailCatalogRetryAction: "Retry", + workbenchSettingsAgentDetailCatalogSettingsAction: "Shared Settings", workbenchSettingsAgentDetailHistoryTitle: "History", workbenchSettingsAgentDetailHistoryHint: "Every change to this agent's instructions and capabilities, oldest actions first.", diff --git a/packages/chat-ui/src/workbench-settings/agents-section.tsx b/packages/chat-ui/src/workbench-settings/agents-section.tsx index ee45f8040..d5f8e2ee1 100644 --- a/packages/chat-ui/src/workbench-settings/agents-section.tsx +++ b/packages/chat-ui/src/workbench-settings/agents-section.tsx @@ -238,20 +238,29 @@ function AgentDetailEditor({ const [catalogState, setCatalogState] = useState({ kind: "loading", }); + const [catalogReloadKey, setCatalogReloadKey] = useState(0); useEffect(() => { let cancelled = false; + setCatalogState({ kind: "loading" }); getResolvedCatalog(tenantId) .then((models) => { if (!cancelled) setCatalogState({ kind: "ready", models }); }) - .catch(() => { - if (!cancelled) setCatalogState({ kind: "error" }); + .catch((cause: unknown) => { + if (cancelled) return; + setCatalogState({ + kind: "error", + message: describeChatError( + cause, + CHAT_STRINGS.workbenchSettingsAgentDetailCatalogError, + ), + }); }); return () => { cancelled = true; }; - }, [tenantId]); + }, [tenantId, catalogReloadKey]); useEffect(() => { let cancelled = false; @@ -345,6 +354,7 @@ function AgentDetailEditor({ agent={agent} detail={state.detail} catalogState={catalogState} + onRetryCatalog={() => setCatalogReloadKey((value) => value + 1)} onChanged={(detail) => setState({ kind: "ready", detail })} /> @@ -430,7 +440,7 @@ type ModelOption = { readonly canonicalName: string; readonly label: string }; type CatalogState = | { readonly kind: "loading" } - | { readonly kind: "error" } + | { readonly kind: "error"; readonly message: string } | { readonly kind: "ready"; readonly models: readonly ModelInfo[] }; /** Only a model with at least one offering is actually launchable for this @@ -479,6 +489,7 @@ function ModelSelect({ agent, detail, catalogState, + onRetryCatalog, onChanged, }: { readonly tenantId: string; @@ -486,6 +497,7 @@ function ModelSelect({ readonly agent: WorkbenchAgent; readonly detail: AgentDetail; readonly catalogState: CatalogState; + readonly onRetryCatalog: () => void; readonly onChanged: (detail: AgentDetail) => void; }) { const [saving, setSaving] = useState(false); @@ -534,7 +546,11 @@ function ModelSelect({