fix(app): keep Console sign-in visible when a Zen API key is stored (#50763)

This commit is contained in:
Luke Parker
2026-09-23 10:19:54 +10:00
committed by GitHub
parent ddeb19790a
commit 3bf8a5a8cf
3 changed files with 109 additions and 3 deletions
@@ -0,0 +1,94 @@
import { expect, test, type Page } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
const directory = "C:/Projects/providers-console-signin"
const project = {
id: "proj_providers_console",
canonical: directory,
name: "Providers console",
vcs: "git",
time: { created: 1700000000000, updated: 1700000000000 },
sandboxes: [],
}
// Zen and the Console account share the id `opencode`: the provider comes from models.dev, the
// integration is renamed by OpencodePlugin and carries the account sign-in.
const zen = (paid: boolean) => ({
id: "opencode",
name: "OpenCode Zen",
models: {
"claude-sonnet-4-6": {
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
cost: { input: paid ? 3 : 0, output: 0 },
},
},
})
const integrations = (connections: unknown[]) => [
{
id: "opencode",
name: "OpenCode Console",
methods: [
{ id: "device", type: "oauth", label: "OpenCode Console account" },
{ type: "key", label: "API key (service account)" },
],
connections,
},
{ id: "opencode-go", name: "OpenCode Go", methods: [{ type: "key" }], connections: [] },
{ id: "anthropic", name: "Anthropic", methods: [{ type: "key" }], connections: [] },
]
test.use({ viewport: { width: 1280, height: 900 } })
async function openProviders(page: Page, input: { paid: boolean; connections: unknown[] }) {
await mockOpenCodeServer(page, {
directory,
project,
provider: { all: [zen(input.paid)], connected: ["opencode"], default: {} },
integrations: integrations(input.connections),
sessions: [],
pageMessages: () => ({ items: [] }),
})
await page.addInitScript((directory) => {
localStorage.setItem(
"opencode.global.dat:server",
JSON.stringify({ projects: { local: [{ worktree: directory, expanded: true }] } }),
)
}, directory)
await page.goto("/")
await page.getByRole("button", { name: "Settings", exact: true }).click()
const settings = page.getByTestId("settings-screen")
await settings.getByRole("tab", { name: "Providers", exact: true }).click()
await expect(settings.getByRole("heading", { name: "Popular providers" })).toBeVisible()
return settings
}
test("fresh install offers the Console sign-in", async ({ page }) => {
const settings = await openProviders(page, { paid: false, connections: [] })
await expect(settings.getByText("No connected providers")).toBeVisible()
await expect(settings.getByText("OpenCode Console", { exact: true })).toBeVisible()
})
test("a stored Zen API key keeps the Console sign-in available", async ({ page }) => {
const settings = await openProviders(page, {
paid: true,
connections: [{ type: "credential", id: "cred_v1", label: "API key", method: "key" }],
})
const connected = settings.locator('[data-component="connected-providers-section"]')
await expect(connected.getByText("OpenCode Zen", { exact: true })).toBeVisible()
await expect(settings.getByText("OpenCode Console", { exact: true })).toBeVisible()
})
test("a Console account hides the sign-in row", async ({ page }) => {
const settings = await openProviders(page, {
paid: true,
connections: [{ type: "credential", id: "cred_account", label: "Clara Team", method: "oauth" }],
})
const connected = settings.locator('[data-component="connected-providers-section"]')
await expect(connected.getByText("OpenCode Zen", { exact: true })).toBeVisible()
// Anthropic only exists in the integration fixture, so its row proves the integration list has loaded.
await expect(settings.getByText("Anthropic", { exact: true })).toBeVisible()
await expect(settings.getByText("OpenCode Console", { exact: true })).toHaveCount(0)
})
+5 -2
View File
@@ -9,6 +9,7 @@ export interface MockServerConfig {
server?: string
provider: unknown | (() => unknown)
integrationMethods?: Record<string, unknown[]>
integrations?: unknown[]
onConnectKey?: (input: { integrationID: string; body: unknown }) => void
shells?: unknown[]
configEntries?: unknown[]
@@ -258,11 +259,13 @@ function mockHandlers(config: MockServerConfig, state: { cursors: Map<string, st
model: () => Effect.succeed({ location: location(config), data: currentModels(providerConfig(config)) }),
modelDefault: () =>
Effect.succeed({ location: location(config), data: currentDefaultModel(providerConfig(config)) }),
integrationList: () => Effect.succeed({ location: location(config), data: [] }),
integrationList: () => Effect.succeed({ location: location(config), data: config.integrations ?? [] }),
integrationGet: (ctx) =>
Effect.succeed({
location: location(config),
data: {
data: config.integrations
?.filter(record)
.find((integration) => integration.id === ctx.params.integrationID) ?? {
id: ctx.params.integrationID,
name: ctx.params.integrationID,
methods: config.integrationMethods?.[ctx.params.integrationID] ?? [{ type: "key", label: "API key" }],
@@ -8,6 +8,7 @@ import { useIntegrations } from "@/providers/catalog/integrations"
import { createMemo, type Component, For, Show } from "solid-js"
import { useLanguage } from "@/runtime/i18n/language"
import { useServerSDK } from "@/runtime/server/client"
import { CONSOLE_INTEGRATION } from "@/providers/connect/controller"
import { DialogConnectProvider, useProviderConnectController } from "@/providers/connect/dialog"
import { SettingsList } from "@/settings/list"
import "@/settings/settings.css"
@@ -62,9 +63,17 @@ export const SettingsProviders: Component<{
const popular = createMemo(() => {
const connectedIDs = new Set(connected().map((p) => p.id))
// The Console account (integration `opencode`) shares its id with the Zen provider. A stored API
// key, including one imported from a v1 auth.json, makes Zen "connected" without any account, so
// the Popular list keeps the sign-in row until the active credential is an OAuth grant. Until the
// integration list arrives the row is still the models.dev Zen provider, so dedupe it as before.
const console = integrations.list().find((entry) => entry.id === CONSOLE_INTEGRATION)
const items = providers
.popular()
.filter((p) => !connectedIDs.has(p.id))
.filter((p) => {
if (p.id !== CONSOLE_INTEGRATION || !console) return !connectedIDs.has(p.id)
return console.connections.find((connection) => connection.type === "credential")?.method !== "oauth"
})
.slice()
items.sort((a, b) => popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id))
return items