From 283f6a4999759695a5482120eae5afa4d6ec901a Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 21 Oct 2026 13:45:58 -0700 Subject: [PATCH 1/2] feat(app): say when a Console account has no Go subscription The Console only returns opencode-go for accounts with a Go subscription, so signing in without one left no Go models and nothing said why. Desktop's post-sign-in model list (and the Go row's connected toast) now says 'No Go subscription on this account' with a Subscribe button to opencode.ai/go; the TUI's /models shows the same line as a row where Go models would be, opening opencode.ai/go on Enter. --- packages/app/e2e/regression/settings.spec.ts | 56 +++++++++++++++++++ packages/app/src/providers/connect/dialog.tsx | 27 ++++++++- packages/app/src/runtime/i18n/en.ts | 2 + packages/tui/src/component/dialog-model.tsx | 32 ++++++++++- packages/tui/test/app-lifecycle.test.tsx | 3 + 5 files changed, 118 insertions(+), 2 deletions(-) diff --git a/packages/app/e2e/regression/settings.spec.ts b/packages/app/e2e/regression/settings.spec.ts index 6ac6987f7a0d..44fc6dfd489b 100644 --- a/packages/app/e2e/regression/settings.spec.ts +++ b/packages/app/e2e/regression/settings.spec.ts @@ -1179,6 +1179,62 @@ test("providers: connecting OpenCode Go keeps the Console row in place", async ( await expect(connected).not.toHaveAttribute("data-flashed") }) +test("providers: signing in to OpenCode Go without a subscription says why there are no Go models", async ({ + page, + context, +}) => { + await context.route("https://auth.example.test/**", (route) => route.fulfill({ body: "Sign in" })) + await context.route("https://opencode.ai/go", (route) => route.fulfill({ body: "OpenCode Go" })) + + // The Console only returns `opencode-go` for accounts with a Go subscription; this one has none. + const state = { authorized: false } + const free = { "big-pickle": { id: "big-pickle", name: "Big Pickle", cost: { input: 0, output: 0 } } } + + const { settings } = await open(page, { + provider: () => + state.authorized + ? { + all: [{ id: "opencode", integrationID: "opencode", name: "New User / OpenCode", models: free }], + connected: ["opencode"], + default: {}, + } + : NO_PROVIDER, + integrations: () => [ + { + id: "opencode", + name: "OpenCode Console", + methods: [{ id: "device", type: "oauth", label: "OpenCode Console account" }], + connections: state.authorized + ? [{ type: "credential", id: "cred_console", label: "New User", method: "oauth" }] + : [], + }, + { id: "opencode-go", name: "OpenCode Go", methods: [{ type: "key" }], connections: [] }, + ], + onIntegrationOAuth: () => ({ url: "https://auth.example.test/device?user_code=GOGO-1234" }), + onIntegrationOAuthStatus: () => { + state.authorized = true + + return "complete" + }, + }) + + await settings.getByRole("tab", { name: "Providers", exact: true }).click() + await settings + .locator(".settings-provider-row", { hasText: "OpenCode Go" }) + .getByRole("button", { name: "Connect", exact: true }) + .click() + const signIn = page.waitForEvent("popup") + await page.getByRole("button", { name: "Continue in the browser", exact: true }).click() + await signIn + + const connected = page.getByRole("dialog", { name: "Connected to OpenCode Go" }) + await expect(connected.getByRole("radio", { name: "Big Pickle", exact: true })).toBeVisible() + await expect(connected.getByText("No Go subscription on this account", { exact: true })).toBeVisible() + const subscribe = page.waitForEvent("popup") + await connected.getByRole("button", { name: "Subscribe", exact: true }).click() + await expect(await subscribe).toHaveURL("https://opencode.ai/go") +}) + test("providers: switching Console accounts keeps the list until the new workspace loads", async ({ page }) => { const accounts = [ { type: "credential", id: "cred_clara", label: "Clara", method: "oauth" }, diff --git a/packages/app/src/providers/connect/dialog.tsx b/packages/app/src/providers/connect/dialog.tsx index 15fe5379bf3b..c87dd6259353 100644 --- a/packages/app/src/providers/connect/dialog.tsx +++ b/packages/app/src/providers/connect/dialog.tsx @@ -61,6 +61,8 @@ import "./models.css" const CUSTOM_ID = "_custom" +const GO_URL = "https://opencode.ai/go" + type IntegrationForm = NonNullable[number] type StringForm = Extract @@ -514,11 +516,17 @@ function ProviderConnection(props: { } dialog.close() + const noGo = props.provider === "opencode-go" && noGoSubscription() showToast({ variant: "success", icon: "circle-check", title: language.t("provider.connect.toast.connected.title", { provider: provider().name }), - description: language.t("provider.connect.toast.connected.description", { provider: provider().name }), + description: noGo + ? language.t("provider.connect.models.noGo") + : language.t("provider.connect.toast.connected.description", { provider: provider().name }), + actions: noGo + ? [{ label: language.t("provider.connect.models.subscribe"), onClick: subscribeToGo }] + : undefined, }) }, }) @@ -556,6 +564,12 @@ function ProviderConnection(props: { const managedProviders = createMemo(() => (isConsole ? consoleProviderGroup(connectionProviders()) : undefined)) + // The Console only grants `opencode-go` to accounts with a Go subscription, so a signed-in account without one gets no + // Go models and nothing else would say why. + const noGoSubscription = () => isConsole && !connectionProviders().some((provider) => provider.id === "opencode-go") + + const subscribeToGo = () => platform.openExternal(GO_URL) + // The server loads the Console workspace's providers after the grant lands, so the first refresh // can still show only the free catalog. Poll briefly for the workspace providers before moving on. async function prepareConsoleCatalog(active: () => boolean) { @@ -1501,6 +1515,17 @@ function ProviderConnection(props: { + +

+ {language.t("provider.connect.models.noGo")} + +

+
connected() && !props.providerID) + const noGoSubscription = createMemo(() => { + const scoped = props.providerID ? providers().get(props.providerID) : undefined + if (props.providerID && scoped?.integrationID !== "opencode" && props.providerID !== "opencode") return false + if (providers().has("opencode-go")) return false + const consoleIntegration = data.location.integration.list(location.ref)?.find((item) => item.id === "opencode") + const active = consoleIntegration ? credentialConnections(consoleIntegration)[0] : undefined + return Boolean(active && !active.status) + }) + const options = createMemo(() => { const needle = query().trim() const showSections = showExtra() && needle.length === 0 @@ -178,10 +192,25 @@ function DialogConnectedModels(props: { return results.map((option) => ({ ...option, description: option.providerName, category: undefined })) } + const noGoOptions = noGoSubscription() + ? [ + { + value: NO_GO_SUBSCRIPTION, + title: "No Go subscription on this account", + category: connected() ? "OpenCode Go" : undefined, + footer: newDesigns() ? "subscribe" : "Subscribe", + onSelect() { + Open.openUrl(GO_URL).catch(() => {}) + }, + }, + ] + : [] + return [ ...favoriteOptions, ...recentOptions, ...modelOptions.filter((option) => option.category !== "Free"), + ...noGoOptions, ...modelOptions.filter((option) => option.category === "Free"), ] }) @@ -217,6 +246,7 @@ function DialogConnectedModels(props: { title: newDesigns() ? (option) => (isFavorite(option?.value) ? "unfavorite" : "favorite") : "Favorite", hidden: !connected(), onTrigger: (option) => { + if (option.value === NO_GO_SUBSCRIPTION) return local.model.toggleFavorite(option.value as { providerID: string; modelID: string }) }, }, diff --git a/packages/tui/test/app-lifecycle.test.tsx b/packages/tui/test/app-lifecycle.test.tsx index d3050c43cefb..152c256bbc1f 100644 --- a/packages/tui/test/app-lifecycle.test.tsx +++ b/packages/tui/test/app-lifecycle.test.tsx @@ -2325,6 +2325,9 @@ test("free usage limit Subscribe handles mid-auth/mid-sync Escape, sync failure, const oauthCallsBeforeCachedExpired = oauthConnectCalls setup.mockInput.pressEnter() await setup.waitForFrame((frame) => frame.includes("Select model") && frame.includes("Big Pickle")) + // Signed in without a Go subscription: the picker says why there are no Go models. + await setup.waitForFrame((frame) => frame.includes("No Go subscription on this account")) + expect(setup.captureCharFrame()).toMatch(/subscribe/i) expect(oauthConnectCalls).toBe(oauthCallsBeforeCachedExpired + 1) setup.mockInput.pressEscape() await setup.waitForFrame((frame) => !frame.includes("Select model")) From e64b59fc5c8d6850cf859fe3eaec739ef0efd811 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 21 Oct 2026 16:25:22 -0700 Subject: [PATCH 2/2] fix(app): detect a missing Go subscription by its models, not the catalog's provider The models.dev catalog lists OpenCode Go even when the Console doesn't grant it, so the provider check never fired. Check for opencode-go models instead (like the TUI upsell) and show the Desktop line above the model list. --- packages/app/e2e/regression/settings.spec.ts | 6 ++++- packages/app/src/providers/connect/dialog.tsx | 25 ++++++++++--------- packages/tui/src/component/dialog-model.tsx | 2 +- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/app/e2e/regression/settings.spec.ts b/packages/app/e2e/regression/settings.spec.ts index 44fc6dfd489b..0476c90b094a 100644 --- a/packages/app/e2e/regression/settings.spec.ts +++ b/packages/app/e2e/regression/settings.spec.ts @@ -1194,7 +1194,11 @@ test("providers: signing in to OpenCode Go without a subscription says why there provider: () => state.authorized ? { - all: [{ id: "opencode", integrationID: "opencode", name: "New User / OpenCode", models: free }], + // The catalog still lists OpenCode Go, unconnected and without models. + all: [ + { id: "opencode", integrationID: "opencode", name: "New User / OpenCode", models: free }, + { id: "opencode-go", name: "OpenCode Go", models: {} }, + ], connected: ["opencode"], default: {}, } diff --git a/packages/app/src/providers/connect/dialog.tsx b/packages/app/src/providers/connect/dialog.tsx index c87dd6259353..0453fe8a87bf 100644 --- a/packages/app/src/providers/connect/dialog.tsx +++ b/packages/app/src/providers/connect/dialog.tsx @@ -566,7 +566,8 @@ function ProviderConnection(props: { // The Console only grants `opencode-go` to accounts with a Go subscription, so a signed-in account without one gets no // Go models and nothing else would say why. - const noGoSubscription = () => isConsole && !connectionProviders().some((provider) => provider.id === "opencode-go") + const noGoSubscription = () => + isConsole && !(data.location.model.list(location()) ?? []).some((model) => model.providerID === "opencode-go") const subscribeToGo = () => platform.openExternal(GO_URL) @@ -1449,6 +1450,17 @@ function ProviderConnection(props: {

{language.t("provider.connect.models.description")}

+ +

+ {language.t("provider.connect.models.noGo")} + +

+
- -

- {language.t("provider.connect.models.noGo")} - -

-
{ const scoped = props.providerID ? providers().get(props.providerID) : undefined if (props.providerID && scoped?.integrationID !== "opencode" && props.providerID !== "opencode") return false - if (providers().has("opencode-go")) return false + if (models().some((model) => model.providerID === "opencode-go")) return false const consoleIntegration = data.location.integration.list(location.ref)?.find((item) => item.id === "opencode") const active = consoleIntegration ? credentialConnections(consoleIntegration)[0] : undefined return Boolean(active && !active.status)