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 a3b698dd9aff8..4af971d54728b 100644 --- a/apps/mobile/src/features/settings/SettingsProviderAccountsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsProviderAccountsRouteScreen.tsx @@ -248,17 +248,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 72221ff3187b4..f5cc389fcc9eb 100644 --- a/apps/server/src/provider/ClaudeProvider.ts +++ b/apps/server/src/provider/ClaudeProvider.ts @@ -790,7 +790,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 2e56eede84cd6..7db5cacce8fbc 100644 --- a/apps/server/src/provider/CodexProvider.ts +++ b/apps/server/src/provider/CodexProvider.ts @@ -694,7 +694,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/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([]); diff --git a/apps/server/src/provider/Drivers/CodexManagedProvider.ts b/apps/server/src/provider/Drivers/CodexManagedProvider.ts index ab5e7add556a9..ea95c86a17264 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 5c53f76237b6a..f4e990b0e807b 100644 --- a/apps/server/src/provider/ProviderRegistry.test.ts +++ b/apps/server/src/provider/ProviderRegistry.test.ts @@ -1189,7 +1189,7 @@ it.layer( } satisfies ServerProvider; const pendingProvider = { ...cachedProvider, - status: "warning", + status: "checking", installed: false, auth: { status: "unknown" }, models: [customModel], @@ -2908,7 +2908,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/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 9a2e8e7f65734..48c3a68f6c99f 100644 --- a/apps/web/src/components/chat/ProviderStatusBanner.test.ts +++ b/apps/web/src/components/chat/ProviderStatusBanner.test.ts @@ -26,7 +26,31 @@ 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, status: "warning" }, null)).toBe(true); + expect(shouldShowProviderStatusBanner({ ...checking, status: "error" }, null)).toBe(true); + 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 43b0102d8816f..80011e96ff197 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/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 diff --git a/packages/contracts/src/server.ts b/packages/contracts/src/server.ts index 40a1aa8b2ca86..17df96855d525 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([