diff --git a/ui/src/e2e/inference-setup-gate.e2e.test.ts b/ui/src/e2e/inference-setup-gate.e2e.test.ts index dffcb95ee624..0d24a94c5a99 100644 --- a/ui/src/e2e/inference-setup-gate.e2e.test.ts +++ b/ui/src/e2e/inference-setup-gate.e2e.test.ts @@ -24,6 +24,7 @@ async function captureProof(page: import("playwright").Page, fileName: string) { suite.define(() => { it("blocks empty chat home until a model is connected", async () => { const context = await suite.browser.newContext({ + colorScheme: "dark", locale: "en-US", serviceWorkers: "block", viewport: { height: 900, width: 1440 }, @@ -38,10 +39,10 @@ suite.define(() => { await expect.poll(() => page.locator(".agent-chat__composer-shell").count()).toBe(0); await expect.poll(() => page.locator("textarea").count()).toBe(0); await expect - .poll(() => page.getByRole("button", { name: "Configure a provider" }).count()) + .poll(() => page.getByRole("button", { name: "Connect an AI provider" }).count()) .toBe(1); await captureProof(page, "chat-home-desktop.png"); - await page.getByRole("button", { name: "Configure a provider" }).click(); + await page.getByRole("button", { name: "Connect an AI provider" }).click(); await expect.poll(() => new URL(page.url()).pathname).toBe("/settings/model-setup"); } finally { await context.close(); @@ -50,6 +51,7 @@ suite.define(() => { it("blocks the new-session composer until a model is connected", async () => { const context = await suite.browser.newContext({ + colorScheme: "dark", locale: "en-US", serviceWorkers: "block", viewport: { height: 900, width: 1440 }, @@ -64,7 +66,7 @@ suite.define(() => { await expect.poll(() => page.locator(".new-session-page__composer").count()).toBe(0); await expect.poll(() => page.locator("textarea").count()).toBe(0); await captureProof(page, "new-session-desktop.png"); - await page.getByRole("button", { name: "Configure a provider" }).click(); + await page.getByRole("button", { name: "Connect an AI provider" }).click(); await expect.poll(() => new URL(page.url()).pathname).toBe("/settings/model-setup"); } finally { await context.close(); @@ -73,6 +75,7 @@ suite.define(() => { it("shows the setup splash before starting custodian chat", async () => { const context = await suite.browser.newContext({ + colorScheme: "dark", locale: "en-US", serviceWorkers: "block", viewport: { height: 900, width: 1660 }, @@ -98,7 +101,7 @@ suite.define(() => { await page.setViewportSize({ height: 520, width: 900 }); await expect - .poll(() => page.getByRole("button", { name: "Configure a provider" }).isVisible()) + .poll(() => page.getByRole("button", { name: "Connect an AI provider" }).isVisible()) .toBe(true); await expect .poll(() => @@ -110,7 +113,7 @@ suite.define(() => { await captureProof(page, "custodian-short-window.png"); await page.setViewportSize({ height: 900, width: 1660 }); - await page.getByRole("button", { name: "Configure a provider" }).click(); + await page.getByRole("button", { name: "Connect an AI provider" }).click(); await expect.poll(() => new URL(page.url()).pathname).toBe("/settings/model-setup"); const modelsLink = page.locator('.settings-sidebar__item[href="/settings/model-providers"]'); await expect.poll(() => modelsLink.getAttribute("aria-current")).toBe("page"); @@ -122,6 +125,7 @@ suite.define(() => { it("distinguishes a configured provider that fails its live check", async () => { const context = await suite.browser.newContext({ + colorScheme: "dark", locale: "en-US", serviceWorkers: "block", viewport: { height: 900, width: 1660 }, @@ -156,17 +160,15 @@ suite.define(() => { message: "OpenClaw requires working inference: provider authentication failed", }); - await page - .getByRole("heading", { name: "OpenClaw couldn't use your configured AI" }) - .waitFor(); + await page.getByRole("heading", { name: "Configured AI needs attention" }).waitFor(); await expect.poll(() => page.locator(".agent-chat__composer-shell").count()).toBe(0); await expect - .poll(() => page.getByRole("button", { name: "Check provider settings" }).count()) + .poll(() => page.getByRole("button", { name: "Review connection" }).count()) .toBe(1); await expect.poll(() => page.getByRole("button", { name: "Retry" }).count()).toBe(1); await captureProof(page, "custodian-provider-unavailable.png"); - await page.getByRole("button", { name: "Check provider settings" }).click(); + await page.getByRole("button", { name: "Review connection" }).click(); await expect.poll(() => new URL(page.url()).pathname).toBe("/settings/model-setup"); const modelsLink = page.locator('.settings-sidebar__item[href="/settings/model-providers"]'); await expect.poll(() => modelsLink.getAttribute("aria-current")).toBe("page"); diff --git a/ui/src/e2e/model-providers.e2e.test.ts b/ui/src/e2e/model-providers.e2e.test.ts index b4954127ab02..1112fb1380ff 100644 --- a/ui/src/e2e/model-providers.e2e.test.ts +++ b/ui/src/e2e/model-providers.e2e.test.ts @@ -125,7 +125,9 @@ describeControlUiE2e("Control UI Models mocked Gateway E2E", () => { const openaiCard = page.locator('[data-provider-id="openai"]'); const readiness = page.locator('[data-model-readiness="model-required"]'); await readiness.waitFor(); - await expect.poll(async () => readiness.textContent()).toContain("Connect your AI"); + await expect + .poll(async () => readiness.textContent()) + .toContain("Connect a verified AI model"); await expect.poll(async () => readiness.textContent()).toContain("No models available"); await expect.poll(async () => openaiCard.textContent()).toContain("Signed in"); expect(await page.locator(".model-providers__defaults").count()).toBe(0); diff --git a/ui/src/e2e/model-setup.e2e.test.ts b/ui/src/e2e/model-setup.e2e.test.ts index 2a57414dcc8e..357c0aacff67 100644 --- a/ui/src/e2e/model-setup.e2e.test.ts +++ b/ui/src/e2e/model-setup.e2e.test.ts @@ -94,7 +94,7 @@ describeControlUiE2e("Control UI Model Setup mocked Gateway E2E", () => { try { const response = await page.goto(`${server.baseUrl}settings/model-setup?firstRun=1`); expect(response?.status()).toBe(200); - await page.getByRole("heading", { name: "Connect your AI" }).waitFor(); + await page.getByRole("heading", { name: "Connect a verified AI model" }).waitFor(); const candidate = page.locator('[data-candidate-kind="codex-cli"]'); await expect.poll(() => candidate.locator('[data-provider-icon="codex"]').count()).toBe(1); await candidate.getByRole("button", { name: "Test & use" }).click(); @@ -413,7 +413,7 @@ describeControlUiE2e("Control UI Model Setup mocked Gateway E2E", () => { } }); - it("turns an unverifiable Gemini CLI login into direct recovery actions", async () => { + it("offers supported Google setup in an accessible provider picker", async () => { const context = await browser.newContext({ colorScheme: "dark", locale: "en-US", @@ -427,24 +427,12 @@ describeControlUiE2e("Control UI Model Setup mocked Gateway E2E", () => { "chat.startup", "openclaw.setup.detect", "openclaw.setup.activate", - "openclaw.setup.auth.start", "openclaw.setup.prepare.start", ], methodResponses: { "openclaw.setup.detect": { candidates: [], - unavailableCandidates: [ - { - id: "gemini-cli", - brandId: "google-gemini-cli", - label: "Gemini CLI", - detail: "installed; login status unavailable", - reason: - "OpenClaw cannot confirm whether this private Gemini CLI login works without starting a session that may expose tools. Sign in through OpenClaw or use a Gemini API key to create a connection it can verify.", - authOptionId: "google-gemini-cli", - manualProviderId: "gemini-api-key", - }, - ], + unavailableCandidates: [], manualProviders: [ { id: "qwen-cn", @@ -470,20 +458,11 @@ describeControlUiE2e("Control UI Model Setup mocked Gateway E2E", () => { id: "gemini-api-key", brandId: "google", groupLabel: "Google", - label: "Google Gemini API key", - hint: "Use an AI Studio API key.", - }, - ], - authOptions: [ - { - id: "google-gemini-cli", - brandId: "google-gemini-cli", - label: "Gemini CLI OAuth", - groupLabel: "Google", - kind: "oauth", - featured: true, + label: "Google AI Studio API key", + hint: "Supported API-key access from aistudio.google.com/apikey", }, ], + authOptions: [], workspace: "/tmp/openclaw-e2e", setupComplete: false, }, @@ -493,20 +472,15 @@ describeControlUiE2e("Control UI Model Setup mocked Gateway E2E", () => { latencyMs: 412, lines: ["Model ready"], }, - "openclaw.setup.auth.start": { - sessionId: "gemini-oauth-session", - done: false, - status: "running", - }, }, }); try { const response = await page.goto(`${server.baseUrl}settings/model-setup`); expect(response?.status()).toBe(200); - await page.getByRole("heading", { name: "Found, but needs attention" }).waitFor(); - await page.getByRole("button", { name: "Sign in with Google" }).waitFor(); - await page.getByRole("button", { name: "Use API key" }).waitFor(); + await page.getByRole("heading", { name: "Connect a verified AI model" }).waitFor(); + await expect.poll(() => page.getByText("Gemini CLI OAuth").count()).toBe(0); + await expect.poll(() => page.getByText("Found, but needs attention").count()).toBe(0); const providerPicker = page.locator(".model-setup-provider-select"); const providerTrigger = providerPicker.locator(".model-setup-provider-select__trigger"); @@ -630,11 +604,16 @@ describeControlUiE2e("Control UI Model Setup mocked Gateway E2E", () => { .toBe(true); await accessValue.fill("sk-old-provider-secret"); - await page.getByRole("button", { name: "Use API key" }).click(); + await providerTrigger.click(); + await expect.poll(manualProviderMenuReady).toBe(true); + const googleProviderHidden = waitForProviderHide(); + await page.locator('[data-manual-provider="gemini-api-key"]').click(); + await googleProviderHidden; await expect.poll(() => providerTrigger.textContent()).toContain("Google"); + await expect.poll(() => providerTrigger.textContent()).toContain("AI Studio API key"); await expect.poll(() => accessValue.inputValue()).toBe(""); await expect - .poll(() => accessValue.evaluate((element) => element === document.activeElement)) + .poll(() => providerTrigger.evaluate((element) => element === document.activeElement)) .toBe(true); await providerTrigger.click(); @@ -696,21 +675,13 @@ describeControlUiE2e("Control UI Model Setup mocked Gateway E2E", () => { await expect .poll(async () => (await gateway.getRequests("openclaw.setup.detect")).length) .toBe(detectCountBeforeDismiss + 1); - await page.getByRole("button", { name: "Use API key" }).click(); + await providerTrigger.click(); + await expect.poll(manualProviderMenuReady).toBe(true); + const googleProviderHiddenAfterDismiss = waitForProviderHide(); + await page.locator('[data-manual-provider="gemini-api-key"]').click(); + await googleProviderHiddenAfterDismiss; await expect.poll(() => providerTrigger.textContent()).toContain("Google"); - - const detectCount = (await gateway.getRequests("openclaw.setup.detect")).length; - await page - .locator('[data-unavailable-candidate="gemini-cli"]') - .getByRole("button", { name: "Check again" }) - .click(); - await expect - .poll(async () => (await gateway.getRequests("openclaw.setup.detect")).length) - .toBe(detectCount + 1); - - await page.getByRole("button", { name: "Sign in with Google" }).click(); - const start = await gateway.waitForRequest("openclaw.setup.auth.start"); - expect(start.params).toMatchObject({ authChoice: "google-gemini-cli" }); + await expect.poll(() => page.getByText("Gemini CLI OAuth").count()).toBe(0); } finally { await context.close(); } diff --git a/ui/src/i18n/locales/en.ts b/ui/src/i18n/locales/en.ts index 11bad9bd2bd5..514815c87052 100644 --- a/ui/src/i18n/locales/en.ts +++ b/ui/src/i18n/locales/en.ts @@ -1960,18 +1960,18 @@ export const en: TranslationMap = { plugin: "Plugin-provided panel.", }, modelSetup: { - heading: "Connect your AI", + heading: "Connect a verified AI model", intro: - "OpenClaw reuses AI access you already have — a CLI login, an API key, or a provider sign-in.", + "OpenClaw checks the AI access available on this Gateway and verifies the exact model before it enables conversations.", required: { title: "No AI provider configured", - body: "OpenClaw couldn't find a provider and model configured for this agent. Add one before starting a conversation.", - action: "Configure a provider", + body: "We couldn't find a provider and model configured for this agent. Choose a supported connection; OpenClaw will test it before enabling chat.", + action: "Connect an AI provider", }, connectionFailure: { - title: "OpenClaw couldn't use your configured AI", - body: "This agent has a provider and model selected, but the connection failed. Check the provider login or API key, model access, and service status, then try again.", - action: "Check provider settings", + title: "Configured AI needs attention", + body: "OpenClaw found the provider and model selected for this agent, but the live check failed. Your configuration is still intact. Review the credential, model access, or provider status, then verify again.", + action: "Review connection", }, loading: "Checking this Gateway for available AI access…", retry: "Retry", @@ -3923,7 +3923,7 @@ export const en: TranslationMap = { }, readiness: { title: "AI setup", - heading: "Connect your AI", + heading: "Connect a verified AI model", signedInNoModels: "You're signed in, but this account exposes no usable models. Choose another provider or account to continue.", notConfigured: "Choose a provider and verify the model OpenClaw will use.", diff --git a/ui/src/pages/chat/chat-view.test.ts b/ui/src/pages/chat/chat-view.test.ts index dd78ceb3593a..73c1ea3c4053 100644 --- a/ui/src/pages/chat/chat-view.test.ts +++ b/ui/src/pages/chat/chat-view.test.ts @@ -4381,8 +4381,8 @@ describe("chat welcome", () => { canSend: false, disabledBanner: { kind: "composer-replacement", - text: "OpenClaw couldn't find a provider and model configured for this agent. Add one before starting a conversation.", - actionLabel: "Configure a provider", + text: "We couldn't find a provider and model configured for this agent. Choose a supported connection; OpenClaw will test it before enabling chat.", + actionLabel: "Connect an AI provider", onAction: () => undefined, }, modelSetupRequired: true, diff --git a/ui/src/pages/model-providers/view.test.ts b/ui/src/pages/model-providers/view.test.ts index 02718444dccb..f4aabefa6d04 100644 --- a/ui/src/pages/model-providers/view.test.ts +++ b/ui/src/pages/model-providers/view.test.ts @@ -230,7 +230,7 @@ describe("renderModelProviders", () => { ); const readiness = container.querySelector('[data-model-readiness="model-required"]'); - expect(text(readiness)).toContain("Connect your AI"); + expect(text(readiness)).toContain("Connect a verified AI model"); expect(text(readiness)).toContain("No models available"); expect(text(readiness)).toContain("Choose another provider"); expect(container.querySelector(".model-providers__defaults")).toBeNull(); @@ -269,7 +269,7 @@ describe("renderModelProviders", () => { const readiness = container.querySelector('[data-model-readiness="model-required"]'); expect(text(readiness)).toContain("Model required"); - expect(button(readiness!, "Connect your AI")).toBeDefined(); + expect(button(readiness!, "Connect a verified AI model")).toBeDefined(); expect(container.querySelector(".model-providers__defaults")).toBeNull(); }); diff --git a/ui/src/pages/model-setup/view.test.ts b/ui/src/pages/model-setup/view.test.ts index 1e242e73c603..3f6f2f23ac2a 100644 --- a/ui/src/pages/model-setup/view.test.ts +++ b/ui/src/pages/model-setup/view.test.ts @@ -23,13 +23,10 @@ const detected: SystemAgentSetupDetectResult = { ], unavailableCandidates: [ { - id: "gemini-cli", - brandId: "google-gemini-cli", - label: "Gemini CLI", - detail: "installed; login status unavailable", - reason: "OpenClaw could not confirm a usable login.", - authOptionId: "google-gemini-cli", - manualProviderId: "gemini-api-key", + id: "pi-cli", + label: "Pi", + detail: "installed; no setup route available", + reason: "This local runtime must be configured outside OpenClaw.", }, ], manualProviders: [ @@ -37,8 +34,8 @@ const detected: SystemAgentSetupDetectResult = { id: "gemini-api-key", brandId: "google", groupLabel: "Google", - label: "Google Gemini API key", - hint: "Use an AI Studio API key.", + label: "Google AI Studio API key", + hint: "Supported API-key access from aistudio.google.com/apikey", }, { id: "openai", @@ -50,15 +47,6 @@ const detected: SystemAgentSetupDetectResult = { }, ], authOptions: [ - { - id: "google-gemini-cli", - brandId: "google-gemini-cli", - label: "Gemini CLI OAuth", - groupLabel: "Google", - kind: "oauth", - featured: true, - hint: "Continue with Google.", - }, { id: "openai-oauth", brandId: "openai", @@ -175,12 +163,12 @@ describe("renderModelSetup", () => { it("renders candidate, unavailable, sign-in, and manual sections", () => { const container = mount(props()); - expect(text(container)).toContain("Connect your AI"); + expect(text(container)).toContain("Connect a verified AI model"); expect(text(container)).toContain("Found on this Gateway"); expect(text(container)).toContain("Codex CLI"); expect(text(container)).toContain("openai/gpt-5 · Signed in locally"); expect(text(container)).toContain("Found, but needs attention"); - expect(text(container)).toContain("OpenClaw could not confirm a usable login"); + expect(text(container)).toContain("This local runtime must be configured outside OpenClaw"); expect(text(container)).toContain("Sign in with a provider"); expect(text(container)).toContain("Set up a local model"); expect(text(container)).toContain("Connect with an API key or token"); @@ -192,9 +180,6 @@ describe("renderModelSetup", () => { ); expect(container.querySelector('input[type="password"]')).not.toBeNull(); expect(container.querySelector("details")?.open).toBe(false); - expect( - container.querySelector('[data-unavailable-candidate="gemini-cli"] [data-provider-icon]'), - ).not.toBeNull(); expect( container.querySelector('[data-candidate-kind="codex-cli"] [data-provider-icon="codex"]'), ).not.toBeNull(); @@ -462,26 +447,16 @@ describe("renderModelSetup", () => { expect(onSuccessClose).toHaveBeenCalledOnce(); }); - it("offers direct recovery actions for an unavailable provider", () => { - const onStartAuth = vi.fn(); - const onUseManualProvider = vi.fn(); + it("only rechecks unavailable runtimes without a supported setup route", () => { const onDetect = vi.fn(); - const container = mount(props({ onStartAuth, onUseManualProvider, onDetect })); + const container = mount(props({ onDetect })); const buttons = container.querySelectorAll( - '[data-unavailable-candidate="gemini-cli"] button', + '[data-unavailable-candidate="pi-cli"] button', ); - expect([...buttons].map((button) => button.textContent?.trim())).toEqual([ - "Sign in with Google", - "Use API key", - "Check again", - ]); + expect([...buttons].map((button) => button.textContent?.trim())).toEqual(["Check again"]); buttons[0]?.click(); - buttons[1]?.click(); - buttons[2]?.click(); - expect(onStartAuth).toHaveBeenCalledWith(expect.objectContaining({ id: "google-gemini-cli" })); - expect(onUseManualProvider).toHaveBeenCalledWith("gemini-api-key"); expect(onDetect).toHaveBeenCalledOnce(); });