fix(app): keep the Console sign-in from suspending the page behind it

Load the integration through a guarded store instead of createResource: the
dialog runs under the opener's Suspense boundary, so a pending resource read
blanked the whole route while the integration loaded. Also show the API key
switch directly instead of behind an Advanced toggle.
This commit is contained in:
LukeParkerDev
2026-09-22 07:30:24 +10:00
parent ee0a316593
commit f2f79a7782
3 changed files with 37 additions and 29 deletions
@@ -1,9 +1,14 @@
import type { FormAnswer, IntegrationMethod, IntegrationOauthConnectOutput } from "@opencode/client/promise"
import type {
FormAnswer,
IntegrationInfo,
IntegrationMethod,
IntegrationOauthConnectOutput,
} from "@opencode/client/promise"
import { useLanguage } from "@/runtime/i18n/language"
import { usePlatform } from "@/runtime/platform/platform"
import { useServerSDK } from "@/runtime/server/client"
import { useData } from "@/runtime/server/current"
import { createEffect, createMemo, createResource, onCleanup } from "solid-js"
import { createEffect, createMemo, on, onCleanup } from "solid-js"
import { createStore, produce } from "solid-js/store"
export type ProviderConnectMethod = Extract<IntegrationMethod, { type: "key" | "oauth" }>
@@ -42,12 +47,28 @@ export function createProviderConnectionController(options: {
const directory = options.directory()
return directory ? { directory } : undefined
}
const [integration] = createResource(
() => ({ provider: options.provider(), directory: options.directory() }),
(input) =>
serverSDK.api.integration
.get({ integrationID: input.provider, location: location() })
.then((result) => result.data),
// Not createResource: the dialog is owned by whichever page opened it, so reading a pending
// resource here would suspend that page's <Suspense> and blank the screen behind the dialog.
const [integration, setIntegration] = createStore({
loading: true,
latest: undefined as IntegrationInfo | undefined,
})
createEffect(
on(
() => ({ provider: options.provider(), directory: options.directory() }),
(input) => {
setIntegration({ loading: true, latest: undefined })
serverSDK.api.integration
.get({ integrationID: input.provider, location: location() })
.then((result) => result.data)
.catch(() => undefined)
.then((latest) => {
if (polling.disposed) return
if (input.provider !== options.provider() || input.directory !== options.directory()) return
setIntegration({ loading: false, latest })
})
},
),
)
const methods = createMemo<ProviderConnectMethod[]>(() => {
const values = integration.latest?.methods.filter(
+8 -20
View File
@@ -710,30 +710,18 @@ function ProviderConnection(props: {
)
}
// Deliberately quiet: most people should never need a key, so it only appears after
// opening "Advanced".
function ConsoleAdvanced() {
const [store, setStore] = createStore({ open: false })
// Deliberately quiet: most people should never need a key, so this stays small and at the bottom.
function ConsoleApiKeySwitch() {
return (
<div class="mt-auto flex flex-col items-end gap-1 px-3 pt-4 text-[11px] leading-4 text-v2-text-text-muted">
<div class="mt-auto flex justify-end px-3 pt-4 text-[11px] leading-4 text-v2-text-text-muted">
<button
type="button"
class="rounded-xs px-1 hover:text-v2-text-text-base focus-visible:outline-2 focus-visible:outline-v2-border-border-focus"
aria-expanded={store.open}
onClick={() => setStore("open", (open) => !open)}
data-action="provider-connect-api-key"
class="rounded-xs px-1 underline decoration-v2-border-border-base underline-offset-2 hover:text-v2-text-text-base focus-visible:outline-2 focus-visible:outline-v2-border-border-focus"
onClick={() => void controller.auth.select(keyIndex())}
>
{language.t("provider.connect.console.advanced")}
{language.t("provider.connect.console.apiKey.switch")}
</button>
<Show when={store.open}>
<button
type="button"
data-action="provider-connect-api-key"
class="rounded-xs px-1 underline decoration-v2-border-border-base underline-offset-2 hover:text-v2-text-text-base focus-visible:outline-2 focus-visible:outline-v2-border-border-focus"
onClick={() => void controller.auth.select(keyIndex())}
>
{language.t("provider.connect.console.apiKey.switch")}
</button>
</Show>
</div>
)
}
@@ -788,7 +776,7 @@ function ProviderConnection(props: {
</Match>
</Switch>
<Show when={isConsole() && controller.currentMethod()?.type !== "key" && keyIndex() !== -1}>
<ConsoleAdvanced />
<ConsoleApiKeySwitch />
</Show>
</div>
</div>
-1
View File
@@ -237,7 +237,6 @@ export const dict = {
"provider.connect.console.description":
"Sign in with your OpenCode Console account. Your browser opens to the Console, where you pick a workspace and select Authorize.",
"provider.connect.console.opening": "Opening your browser…",
"provider.connect.console.advanced": "Advanced",
"provider.connect.console.apiKey.switch": "Connect with an API key or service account instead",
"provider.connect.console.apiKey.description":
"Paste an API key for a service account. You create service accounts in the OpenCode Console under Keys.",