diff --git a/src/__tests__/bugfixes.test.ts b/src/__tests__/bugfixes.test.ts
index 3177b7d868..1a7e95d54d 100644
--- a/src/__tests__/bugfixes.test.ts
+++ b/src/__tests__/bugfixes.test.ts
@@ -695,3 +695,55 @@ describe('Dev-channels dialog coverage', () => {
)
})
})
+
+// ---------------------------------------------------------------------------
+// Fix: onboarding + trust dialog skipped entirely for third-party providers
+// ---------------------------------------------------------------------------
+// Behavioral coverage lives in src/utils/setupScreenGates.test.ts — the
+// gating decisions were extracted into that provider-free seam because this
+// module's import chain cannot be loaded under bun test (compile-time
+// feature() macro checker, same constraint as the dev-channels tests above).
+// These wiring checks assert showSetupScreens actually consults the seam and
+// that no provider gate was re-introduced around the dialogs.
+describe('Onboarding and trust dialog — third-party providers', () => {
+ test('showSetupScreens routes both dialogs through the provider-free seam', async () => {
+ const content = await file('interactiveHelpers.tsx').text()
+
+ expect(content).toContain('getRequiredSetupScreens({')
+ expect(content).toContain('if (setupScreens.onboarding)')
+ expect(content).toContain('if (setupScreens.trustDialog)')
+ })
+
+ test('the env-config option never renders the raw endpoint', async () => {
+ // OPENAI_BASE_URL/OPENAI_API_BASE can carry credentials (userinfo or
+ // token query params) and everything rendered lands in terminal
+ // scrollback. Redaction behavior is tested in envProviderOption.test.ts;
+ // this guards the wiring — the raw `envBaseUrl` may only reach profile
+ // persistence (addProviderProfile/getProviderProfiles/label), never a
+ // rendered label or status message.
+ const content = await file('components/ConsoleOAuthFlow.tsx').text()
+
+ expect(content).toContain('getEnvProviderOption()')
+ // Rendered sites use the redacted value.
+ expect(content).toMatch(/\{envBaseUrlVarName\}=\{envBaseUrlForDisplay\}/)
+ expect(content).toMatch(/\$\{envBaseUrlForDisplay\}\) as your active provider/)
+ // No rendered site interpolates the raw endpoint.
+ expect(content).not.toMatch(/\{envBaseUrl\}/)
+ expect(content).not.toMatch(/\$\{envBaseUrl\}/)
+ })
+
+ test('no dialog is gated behind usesAnthropicSetup', async () => {
+ const content = await file('interactiveHelpers.tsx').text()
+
+ // Theme choice + security notes are universal, and workspace trust is
+ // orthogonal to the API provider: an untrusted repo is exactly as
+ // dangerous over a local model as over Anthropic. The seam takes no
+ // provider input, so the only way to regress is to add a gate at the
+ // call sites — which this guards against.
+ expect(content).not.toMatch(/usesAnthropicSetup\s*&&\s*\(?\s*setupScreens/)
+ expect(content).not.toMatch(/usesAnthropicSetup\s*&&\s*\(\s*!config\.theme/)
+ expect(content).not.toMatch(
+ /usesAnthropicSetup\s*&&\s*!checkHasTrustDialogAccepted/,
+ )
+ })
+})
diff --git a/src/components/ConsoleOAuthFlow.tsx b/src/components/ConsoleOAuthFlow.tsx
index 65a06ce4e6..d66e91ca63 100644
--- a/src/components/ConsoleOAuthFlow.tsx
+++ b/src/components/ConsoleOAuthFlow.tsx
@@ -11,6 +11,15 @@ import { sendNotification } from '../services/notifier.js';
import { OAuthService } from '../services/oauth/index.js';
import { getOauthAccountInfo, validateForceLoginOrg } from '../utils/auth.js';
import { logError } from '../utils/log.js';
+import { getEnvProviderOption } from '../utils/envProviderOption.js';
+import { getLocalOpenAICompatibleProviderLabel } from '../utils/providerDiscovery.js';
+import { type ProviderProfile } from '../utils/config.js';
+import {
+ addProviderProfile,
+ getProviderProfiles,
+ setActiveProviderProfile,
+ updateProviderProfile,
+} from '../utils/providerProfiles.js';
import { getSettings_DEPRECATED } from '../utils/settings/settings.js';
import { ProviderManager } from './ProviderManager.js';
import { Select } from './CustomSelect/select.js';
@@ -386,7 +395,42 @@ function OAuthStatusMessage({
startingMessage ||
'OpenClaude can be used with your Claude subscription or billed based on API usage through your Console account.'
+ // OPENAI_BASE_URL/OPENAI_MODEL in the environment signal an
+ // OpenAI-compatible setup the user already has — offer to adopt it as
+ // the active provider profile instead of walking them through login for
+ // an account they may never have wanted. Env vars alone do NOT activate
+ // the route (resolveActiveRouteIdFromEnv requires CLAUDE_CODE_USE_OPENAI
+ // or a saved profile), so selecting this saves + activates a profile.
+ // Both fields gate the option because a profile requires baseUrl+model.
+ // getEnvProviderOption owns the secret-disclosure boundary: only
+ // `displayBaseUrl` (redacted) may be rendered — the raw `baseUrl`
+ // exists solely for profile creation/activation. See its tests for
+ // the credential cases.
+ const {
+ available: envConfigAvailable,
+ varName: envBaseUrlVarName,
+ baseUrl: envBaseUrl,
+ displayBaseUrl: envBaseUrlForDisplay,
+ model: envModel,
+ } = getEnvProviderOption()
+
const loginOptions = [
+ ...(envConfigAvailable
+ ? [
+ {
+ label: (
+
+ Use current environment configuration ·{' '}
+
+ {envBaseUrlVarName}={envBaseUrlForDisplay}
+
+ {'\n'}
+
+ ),
+ value: 'environment' as const,
+ },
+ ]
+ : []),
{
label: (
@@ -427,6 +471,68 @@ function OAuthStatusMessage({