From 9b1b2dea61e6ee099bdf3ce7ee693fbe71ecf686 Mon Sep 17 00:00:00 2001 From: Suhaas Badada Date: Thu, 5 Nov 2026 13:33:59 -1000 Subject: [PATCH 1/3] fix(providers): distinguish initial status checks from warnings --- .../SettingsEnvironmentDetailRouteScreen.tsx | 8 ++-- .../SettingsProviderAccountsRouteScreen.tsx | 24 +++++----- apps/server/src/provider/ClaudeProvider.ts | 2 +- apps/server/src/provider/CodexProvider.ts | 2 +- .../provider/Drivers/CodexManagedProvider.ts | 9 ++-- .../src/provider/ProviderRegistry.test.ts | 16 ++++++- .../chat/ProviderStatusBanner.test.ts | 48 ++++++++++++++++++- .../components/chat/ProviderStatusBanner.tsx | 2 +- .../settings/providerStatus.test.ts | 13 +++++ .../src/components/settings/providerStatus.ts | 3 +- .../src/onboarding/providerReadiness.logic.ts | 1 + packages/contracts/src/server.ts | 8 +++- 12 files changed, 110 insertions(+), 26 deletions(-) diff --git a/apps/mobile/src/features/settings/SettingsEnvironmentDetailRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsEnvironmentDetailRouteScreen.tsx index 13c9619768f4e..9b830d7e45f52 100644 --- a/apps/mobile/src/features/settings/SettingsEnvironmentDetailRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsEnvironmentDetailRouteScreen.tsx @@ -343,9 +343,11 @@ function EnvironmentDetail({ environmentId }: { readonly environmentId: Environm - {provider.installed - ? (provider.version ?? "Version unknown") - : "Not installed"} + {provider.status === "checking" + ? "Checking provider status" + : provider.installed + ? (provider.version ?? "Version unknown") + : "Not installed"} {provider.versionAdvisory?.latestVersion ? ` · Latest ${provider.versionAdvisory.latestVersion}` : ""} diff --git a/apps/mobile/src/features/settings/SettingsProviderAccountsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsProviderAccountsRouteScreen.tsx index ff4d44fcaa7d4..7354edd1825e3 100644 --- a/apps/mobile/src/features/settings/SettingsProviderAccountsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsProviderAccountsRouteScreen.tsx @@ -244,17 +244,19 @@ function ProviderAccount({ ? state.message : !provider.enabled ? "Turned off" - : signedIn - ? "Signed in" - : isDiscovering - ? "Discovering sign-in methods…" - : needsExternalSetup - ? "No in-app sign-in advertised. Follow the provider's docs to finish setup." - : !provider.installed - ? "Not installed" - : usesCredentials - ? "Connect to load models" - : "Not signed in"; + : provider.status === "checking" + ? "Checking provider status" + : signedIn + ? "Signed in" + : isDiscovering + ? "Discovering sign-in methods…" + : needsExternalSetup + ? "No in-app sign-in advertised. Follow the provider's docs to finish setup." + : !provider.installed + ? "Not installed" + : usesCredentials + ? "Connect to load models" + : "Not signed in"; // A provider with several sign-in methods picks one from a menu on the same control. const methodActions: MenuAction[] = methods.map((method) => ({ id: `method:${method.id}`, diff --git a/apps/server/src/provider/ClaudeProvider.ts b/apps/server/src/provider/ClaudeProvider.ts index 5ea745622bb34..8f42ae048bef4 100644 --- a/apps/server/src/provider/ClaudeProvider.ts +++ b/apps/server/src/provider/ClaudeProvider.ts @@ -779,7 +779,7 @@ export const makePendingClaudeProvider = Effect.fn("ClaudeProvider.makePendingCl probe: { installed: false, version: null, - status: "warning", + status: "checking", auth: { status: "unknown" }, message: "Claude provider status has not been checked in this session yet.", }, diff --git a/apps/server/src/provider/CodexProvider.ts b/apps/server/src/provider/CodexProvider.ts index 2d546b36a9b15..040b62b38c5a4 100644 --- a/apps/server/src/provider/CodexProvider.ts +++ b/apps/server/src/provider/CodexProvider.ts @@ -690,7 +690,7 @@ const makePendingCodexProvider = Effect.fn("CodexProvider.makePendingCodexProvid probe: { installed: false, version: null, - status: "warning", + status: "checking", auth: { status: "unknown" }, message: "Codex provider status has not been checked in this session yet.", }, diff --git a/apps/server/src/provider/Drivers/CodexManagedProvider.ts b/apps/server/src/provider/Drivers/CodexManagedProvider.ts index f9728f9a24973..7460ea30be8de 100644 --- a/apps/server/src/provider/Drivers/CodexManagedProvider.ts +++ b/apps/server/src/provider/Drivers/CodexManagedProvider.ts @@ -73,10 +73,11 @@ export const makeManagedCodexProvider = Effect.fn("makeManagedCodexProvider")(fu const check = Effect.gen(function* () { const base = yield* pending; if (!enabled) return base; + const checkedBase = { ...base, status: "warning" as const }; const executable = yield* runtime.installation.resolve().pipe(Effect.option); if (Option.isNone(executable)) return { - ...base, + ...checkedBase, installed: false, models: [], message: "Set up Codex to get started.", @@ -85,7 +86,7 @@ export const makeManagedCodexProvider = Effect.fn("makeManagedCodexProvider")(fu const saved = yield* runtime.auth.read.pipe(Effect.orElseSucceed(() => Option.none())); if (Option.isSome(saved) && !saved.value.scopes.includes("chatgpt.tokens.use.direct")) return { - ...base, + ...checkedBase, installed: true, version: executable.value.version, models: [], @@ -99,7 +100,7 @@ export const makeManagedCodexProvider = Effect.fn("makeManagedCodexProvider")(fu }; if (Option.isNone(saved)) return { - ...base, + ...checkedBase, installed: true, version: executable.value.version, models: [], @@ -183,7 +184,7 @@ export const makeManagedCodexProvider = Effect.fn("makeManagedCodexProvider")(fu runtime.auth.read.pipe( Effect.orElseSucceed(() => Option.none()), Effect.map((current) => ({ - ...base, + ...checkedBase, installed: true, version: executable.value.version, models: [], diff --git a/apps/server/src/provider/ProviderRegistry.test.ts b/apps/server/src/provider/ProviderRegistry.test.ts index 9b1187493967c..aff83c618922c 100644 --- a/apps/server/src/provider/ProviderRegistry.test.ts +++ b/apps/server/src/provider/ProviderRegistry.test.ts @@ -44,7 +44,11 @@ import { deepMerge } from "@t3tools/shared/Struct"; import { createModelCapabilities } from "@t3tools/shared/model"; import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings"; -import { checkCodexProviderStatus, type CodexAppServerProviderSnapshot } from "./CodexProvider.ts"; +import { + checkCodexProviderStatus, + makePendingCodexProvider, + type CodexAppServerProviderSnapshot, +} from "./CodexProvider.ts"; import { BUNDLED_CLAUDE_MODEL_CATALOG } from "./ClaudeModelCatalog.ts"; import { checkClaudeProviderStatus } from "./ClaudeProvider.ts"; import * as BackgroundPolicy from "../background/BackgroundPolicy.ts"; @@ -360,6 +364,14 @@ it.layer( describe("checkCodexProviderStatus", () => { it.effect("uses the app-server account and model list for provider status", () => Effect.gen(function* () { + const pending = yield* makePendingCodexProvider(defaultCodexSettings); + assert.strictEqual(pending.status, "checking"); + assert.strictEqual(pending.auth.status, "unknown"); + const disabled = yield* makePendingCodexProvider({ + ...defaultCodexSettings, + enabled: false, + }); + assert.strictEqual(disabled.status, "disabled"); const status = yield* checkCodexProviderStatus(defaultCodexSettings, () => Effect.succeed( makeCodexProbeSnapshot({ @@ -2799,7 +2811,7 @@ it.layer( const pendingRebuild = yield* Stream.toPull( codexSnapshots.pipe( - Stream.filter((provider) => provider.status === "warning" && !provider.installed), + Stream.filter((provider) => provider.status === "checking" && !provider.installed), ), ); yield* serverSettings.updateSettings({ diff --git a/apps/web/src/components/chat/ProviderStatusBanner.test.ts b/apps/web/src/components/chat/ProviderStatusBanner.test.ts index 9a2e8e7f65734..d4ba4b2eea7a2 100644 --- a/apps/web/src/components/chat/ProviderStatusBanner.test.ts +++ b/apps/web/src/components/chat/ProviderStatusBanner.test.ts @@ -26,7 +26,53 @@ const provider: ServerProvider = { }, }; -describe("compatibility banners", () => { +describe("provider status banners", () => { + it("keeps initial checks quiet without hiding completed probe failures", () => { + const { compatibilityAdvisory: _compatibilityAdvisory, ...base } = provider; + const checking: ServerProvider = { + ...base, + installed: false, + version: null, + status: "checking", + auth: { status: "unknown" }, + message: "Codex provider status has not been checked in this session yet.", + }; + expect(shouldShowProviderStatusBanner(checking, null)).toBe(false); + expect( + shouldShowProviderStatusBanner({ ...checking, message: "Checking installation." }, null), + ).toBe(false); + expect( + shouldShowProviderStatusBanner( + { ...checking, installed: true, status: "ready", auth: { status: "authenticated" } }, + null, + ), + ).toBe(false); + for (const failure of [ + { status: "error", installed: false, message: "Could not start Codex CLI." }, + { status: "error", installed: true, message: "Codex provider probe failed." }, + { status: "error", installed: true, message: "Timed out while checking Codex." }, + { + status: "warning", + installed: true, + message: "Codex did not return its model catalog in time.", + }, + { status: "warning", installed: false, message: checking.message }, + { status: "error", auth: { status: "unauthenticated" }, message: "Run codex login." }, + ] as const) { + expect(shouldShowProviderStatusBanner({ ...checking, ...failure }, null)).toBe(true); + } + expect(shouldShowProviderStatusBanner({ ...checking, status: "disabled" }, null)).toBe(false); + expect( + shouldShowProviderStatusBanner( + { + ...checking, + compatibilityAdvisory: { ...provider.compatibilityAdvisory!, status: "broken" }, + }, + null, + ), + ).toBe(true); + }); + it("shows and dismisses a warning on a healthy provider, then clears it after policy relaxation", () => { expect(shouldShowProviderStatusBanner(provider, null)).toBe(true); expect(shouldShowProviderStatusBanner(provider, getProviderStatusBannerKey(provider))).toBe( diff --git a/apps/web/src/components/chat/ProviderStatusBanner.tsx b/apps/web/src/components/chat/ProviderStatusBanner.tsx index 64afa55ed0d78..f02f00581ed15 100644 --- a/apps/web/src/components/chat/ProviderStatusBanner.tsx +++ b/apps/web/src/components/chat/ProviderStatusBanner.tsx @@ -34,7 +34,7 @@ export function getProviderStatusBannerKey(status: ServerProvider | null): strin incompatible.message ?? "", ].join("\u0000"); } - if (status.status === "ready") return null; + if (status.status === "ready" || status.status === "checking") return null; // Antigravity validates credentials at session start; unknown restart auth is not failure. if ( status.driver === "antigravity" && diff --git a/apps/web/src/components/settings/providerStatus.test.ts b/apps/web/src/components/settings/providerStatus.test.ts index bc4b9674b674f..18f1292f94a19 100644 --- a/apps/web/src/components/settings/providerStatus.test.ts +++ b/apps/web/src/components/settings/providerStatus.test.ts @@ -22,6 +22,19 @@ const provider: ServerProvider = { }; describe("getProviderSummary", () => { + it("presents an initial check neutrally before installation is known", () => { + const checking: ServerProvider = { + ...provider, + installed: false, + status: "checking", + auth: { status: "unknown" }, + }; + expect(getProviderStatusKey(checking)).toBe("neutral"); + expect(getProviderSummary(checking).headline).toBe("Checking provider status"); + expect(getProviderSummary({ ...checking, status: "error" }).headline).toBe("Not found"); + expect(getProviderSummary({ ...checking, enabled: false }).headline).toBe("Disabled"); + }); + it("does not hide a provider error behind a previous authenticated state", () => { expect( getProviderSummary({ diff --git a/apps/web/src/components/settings/providerStatus.ts b/apps/web/src/components/settings/providerStatus.ts index a4db548906549..04ac2f31521dd 100644 --- a/apps/web/src/components/settings/providerStatus.ts +++ b/apps/web/src/components/settings/providerStatus.ts @@ -40,12 +40,13 @@ function isAntigravitySignInUnchecked(provider: ServerProvider): boolean { export function getProviderStatusKey(provider: ServerProvider | undefined): ProviderStatusKey { if (provider && !provider.enabled) return "disabled"; + if (provider?.status === "checking") return "neutral"; if (provider && isAntigravitySignInUnchecked(provider)) return "neutral"; return provider?.status ?? "warning"; } export function getProviderSummary(provider: ServerProvider | undefined) { - if (!provider) { + if (!provider || (provider.enabled && provider.status === "checking")) { return { headline: "Checking provider status", detail: "Waiting for the server to report installation and authentication details.", diff --git a/apps/web/src/onboarding/providerReadiness.logic.ts b/apps/web/src/onboarding/providerReadiness.logic.ts index 6fb427100bdf7..b1035ddb42b15 100644 --- a/apps/web/src/onboarding/providerReadiness.logic.ts +++ b/apps/web/src/onboarding/providerReadiness.logic.ts @@ -36,6 +36,7 @@ function quoteProviderBinary( export function getOnboardingProviderState(provider: ServerProvider | undefined) { if (provider === undefined) return "checking"; if (!provider.enabled || provider.status === "disabled") return "disabled"; + if (provider.status === "checking") return "checking"; if (!provider.installed && provider.status === "warning" && provider.auth.status === "unknown") return "checking"; if (!provider.installed) return "install"; diff --git a/packages/contracts/src/server.ts b/packages/contracts/src/server.ts index e027930c116ba..a1c879f08e0fc 100644 --- a/packages/contracts/src/server.ts +++ b/packages/contracts/src/server.ts @@ -50,7 +50,13 @@ export type ServerConfigIssue = typeof ServerConfigIssue.Type; const ServerConfigIssues = ForwardCompatibleArray(ServerConfigIssue); -export const ServerProviderState = Schema.Literals(["ready", "warning", "error", "disabled"]); +export const ServerProviderState = Schema.Literals([ + "checking", + "ready", + "warning", + "error", + "disabled", +]); export type ServerProviderState = typeof ServerProviderState.Type; export const ServerProviderAuthStatus = Schema.Literals([ From 4d495e5549b354a4a21a32a86159386e04be58e2 Mon Sep 17 00:00:00 2001 From: Suhaas Badada Date: Thu, 5 Nov 2026 13:45:43 -1000 Subject: [PATCH 2/3] test(providers): preserve managed Codex sign-out warning --- apps/server/src/provider/Drivers/CodexDriver.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/server/src/provider/Drivers/CodexDriver.test.ts b/apps/server/src/provider/Drivers/CodexDriver.test.ts index 868dc9c0af03b..292750a49bb84 100644 --- a/apps/server/src/provider/Drivers/CodexDriver.test.ts +++ b/apps/server/src/provider/Drivers/CodexDriver.test.ts @@ -292,6 +292,7 @@ it.layer(layerTest)("CodexDriver", (it) => { yield* instance.auth!.logout(Effect.void); const after = Option.getOrThrow(yield* Fiber.join(disconnected)); expect(after.auth.status).toBe("unauthenticated"); + expect(after.status).toBe("warning"); expect(after.auth.email).toBeUndefined(); expect(after.installed).toBe(true); expect(after.models).toEqual([]); From 6933eb06a6cfe6c5f78ccc8911685b930b858be0 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 03:29:15 +0000 Subject: [PATCH 3/3] fix(providers): keep cached models and prompt links while checking The pending Codex snapshot moved from warning to checking, so the registry stopped retaining cached models during the initial probe and prompt links could not resolve a provider until it finished. Treat checking like the old pending warning in both places, and trim tests to the regressions they guard. AI-assisted; verified with the tests and tools listed above. --- .../src/provider/ProviderRegistry.test.ts | 16 ++---------- apps/server/src/provider/ProviderRegistry.ts | 4 ++- .../chat/ProviderStatusBanner.test.ts | 26 ++----------------- .../client-runtime/src/promptLinks.test.ts | 8 +++++- packages/client-runtime/src/promptLinks.ts | 2 +- 5 files changed, 15 insertions(+), 41 deletions(-) diff --git a/apps/server/src/provider/ProviderRegistry.test.ts b/apps/server/src/provider/ProviderRegistry.test.ts index 30b51af1493ec..f4e990b0e807b 100644 --- a/apps/server/src/provider/ProviderRegistry.test.ts +++ b/apps/server/src/provider/ProviderRegistry.test.ts @@ -44,11 +44,7 @@ import { deepMerge } from "@t3tools/shared/Struct"; import { createModelCapabilities } from "@t3tools/shared/model"; import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings"; -import { - checkCodexProviderStatus, - makePendingCodexProvider, - type CodexAppServerProviderSnapshot, -} from "./CodexProvider.ts"; +import { checkCodexProviderStatus, type CodexAppServerProviderSnapshot } from "./CodexProvider.ts"; import { BUNDLED_CLAUDE_MODEL_CATALOG } from "./ClaudeModelCatalog.ts"; import { checkClaudeProviderStatus } from "./ClaudeProvider.ts"; import * as BackgroundPolicy from "../background/BackgroundPolicy.ts"; @@ -364,14 +360,6 @@ it.layer( describe("checkCodexProviderStatus", () => { it.effect("uses the app-server account and model list for provider status", () => Effect.gen(function* () { - const pending = yield* makePendingCodexProvider(defaultCodexSettings); - assert.strictEqual(pending.status, "checking"); - assert.strictEqual(pending.auth.status, "unknown"); - const disabled = yield* makePendingCodexProvider({ - ...defaultCodexSettings, - enabled: false, - }); - assert.strictEqual(disabled.status, "disabled"); const status = yield* checkCodexProviderStatus(defaultCodexSettings, () => Effect.succeed( makeCodexProbeSnapshot({ @@ -1201,7 +1189,7 @@ it.layer( } satisfies ServerProvider; const pendingProvider = { ...cachedProvider, - status: "warning", + status: "checking", installed: false, auth: { status: "unknown" }, models: [customModel], diff --git a/apps/server/src/provider/ProviderRegistry.ts b/apps/server/src/provider/ProviderRegistry.ts index a0ea5c78fbc61..35cb286f2c7bd 100644 --- a/apps/server/src/provider/ProviderRegistry.ts +++ b/apps/server/src/provider/ProviderRegistry.ts @@ -218,7 +218,9 @@ const shouldRetainMissingProviderModels = (provider: ServerProvider): boolean => const isPendingAntigravityAuthentication = isAntigravity && provider.status === "warning" && provider.auth.status === "unknown"; const isPendingInitialProbe = - provider.enabled && !provider.installed && provider.status === "warning"; + provider.enabled && + !provider.installed && + (provider.status === "checking" || provider.status === "warning"); const isIncompleteInstalledProbe = provider.installed && (provider.status === "error" || (isCodex && provider.status === "warning")); diff --git a/apps/web/src/components/chat/ProviderStatusBanner.test.ts b/apps/web/src/components/chat/ProviderStatusBanner.test.ts index d4ba4b2eea7a2..48c3a68f6c99f 100644 --- a/apps/web/src/components/chat/ProviderStatusBanner.test.ts +++ b/apps/web/src/components/chat/ProviderStatusBanner.test.ts @@ -38,30 +38,8 @@ describe("provider status banners", () => { message: "Codex provider status has not been checked in this session yet.", }; expect(shouldShowProviderStatusBanner(checking, null)).toBe(false); - expect( - shouldShowProviderStatusBanner({ ...checking, message: "Checking installation." }, null), - ).toBe(false); - expect( - shouldShowProviderStatusBanner( - { ...checking, installed: true, status: "ready", auth: { status: "authenticated" } }, - null, - ), - ).toBe(false); - for (const failure of [ - { status: "error", installed: false, message: "Could not start Codex CLI." }, - { status: "error", installed: true, message: "Codex provider probe failed." }, - { status: "error", installed: true, message: "Timed out while checking Codex." }, - { - status: "warning", - installed: true, - message: "Codex did not return its model catalog in time.", - }, - { status: "warning", installed: false, message: checking.message }, - { status: "error", auth: { status: "unauthenticated" }, message: "Run codex login." }, - ] as const) { - expect(shouldShowProviderStatusBanner({ ...checking, ...failure }, null)).toBe(true); - } - expect(shouldShowProviderStatusBanner({ ...checking, status: "disabled" }, null)).toBe(false); + expect(shouldShowProviderStatusBanner({ ...checking, status: "warning" }, null)).toBe(true); + expect(shouldShowProviderStatusBanner({ ...checking, status: "error" }, null)).toBe(true); expect( shouldShowProviderStatusBanner( { diff --git a/packages/client-runtime/src/promptLinks.test.ts b/packages/client-runtime/src/promptLinks.test.ts index 20e8b2b7df726..21dcaa78129ad 100644 --- a/packages/client-runtime/src/promptLinks.test.ts +++ b/packages/client-runtime/src/promptLinks.test.ts @@ -129,7 +129,7 @@ describe("resolvePromptLinkModelSelection", () => { const provider = ( instanceId: string, driver: string, - status: "ready" | "warning" | "error" | "disabled" = "ready", + status: "checking" | "ready" | "warning" | "error" | "disabled" = "ready", ) => ({ instanceId: ProviderInstanceId.make(instanceId), driver: ProviderDriverKind.make(driver), @@ -168,5 +168,11 @@ describe("resolvePromptLinkModelSelection", () => { expect( resolvePromptLinkModelSelection(providers, { provider: "cursor", model: null }), ).toBeNull(); + expect( + resolvePromptLinkModelSelection([provider("codex", "codex", "checking")], { + provider: "codex", + model: "small", + }), + ).toEqual({ selection: { instanceId: "codex", model: "small" }, exact: true }); }); }); diff --git a/packages/client-runtime/src/promptLinks.ts b/packages/client-runtime/src/promptLinks.ts index edb13ce3f0243..8104a0a4018ac 100644 --- a/packages/client-runtime/src/promptLinks.ts +++ b/packages/client-runtime/src/promptLinks.ts @@ -111,7 +111,7 @@ export function resolvePromptLinkModelSelection( ); const instance = usable.find((provider) => provider.status === "ready") ?? - usable.find((provider) => provider.status === "warning"); + usable.find((provider) => provider.status === "warning" || provider.status === "checking"); if (instance === undefined) return null; const requested = request.model === null