fix(app): preserve provider dialog backdrop (#35370)

Co-authored-by: Jay <53023+jayair@users.noreply.github.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
This commit is contained in:
opencode-agent[bot] 2026-07-06 06:08:59 +00:00 committed by GitHub
commit 14df88eab5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 107 additions and 95 deletions

View file

@ -17,19 +17,17 @@ import { useServerSync } from "@/context/server-sync"
import { useLanguage } from "@/context/language"
import { useProviders } from "@/hooks/use-providers"
export function DialogConnectProvider(props: { provider: string; directory?: Accessor<string | undefined> }) {
export function DialogConnectProvider(props: {
provider: string
directory?: Accessor<string | undefined>
onBack: () => void
}) {
const dialog = useDialog()
const serverSync = useServerSync()
const serverSDK = useServerSDK()
const language = useLanguage()
const providers = useProviders(props.directory)
const all = () => {
void import("./dialog-select-provider").then((x) => {
dialog.show(() => <x.DialogSelectProvider directory={props.directory} />)
})
}
const alive = { value: true }
const timer = { current: undefined as ReturnType<typeof setTimeout> | undefined }
@ -364,19 +362,11 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
}
function goBack() {
if (methods().length === 1) {
all()
return
}
if (store.authorization) {
if (methods().length > 1 && store.methodIndex !== undefined) {
dispatch({ type: "method.reset" })
return
}
if (store.methodIndex !== undefined) {
dispatch({ type: "method.reset" })
return
}
all()
props.onBack()
}
function MethodSelection() {
@ -600,6 +590,7 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
return (
<Dialog
class="h-full"
title={
<IconButton
tabIndex={-1}
@ -609,6 +600,7 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
aria-label={language.t("common.goBack")}
/>
}
transition
>
<div class="flex flex-col gap-6 px-2.5 pb-3">
<div class="px-2.5 flex gap-4 items-center">

View file

@ -6,18 +6,16 @@ import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { useMutation } from "@tanstack/solid-query"
import { TextField } from "@opencode-ai/ui/text-field"
import { showToast } from "@/utils/toast"
import { type Accessor, batch, For } from "solid-js"
import { batch, For } from "solid-js"
import { createStore, produce } from "solid-js/store"
import { Link } from "@/components/link"
import { useServerSDK } from "@/context/server-sdk"
import { useServerSync } from "@/context/server-sync"
import { useLanguage } from "@/context/language"
import { type FormState, headerRow, modelRow, validateCustomProvider } from "./dialog-custom-provider-form"
import { DialogSelectProvider } from "./dialog-select-provider"
type Props = {
back?: "providers" | "close"
directory?: Accessor<string | undefined>
onBack: () => void
}
export function DialogCustomProvider(props: Props) {
@ -36,14 +34,6 @@ export function DialogCustomProvider(props: Props) {
err: {},
})
const goBack = () => {
if (props.back === "close") {
dialog.close()
return
}
dialog.show(() => <DialogSelectProvider directory={props.directory} />)
}
const addModel = () => {
setForm(
"models",
@ -164,12 +154,13 @@ export function DialogCustomProvider(props: Props) {
return (
<Dialog
class="h-full"
title={
<IconButton
tabIndex={-1}
icon="arrow-left"
variant="ghost"
onClick={goBack}
onClick={props.onBack}
aria-label={language.t("common.goBack")}
/>
}

View file

@ -24,7 +24,7 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
const connect = (provider: string) => {
void import("./dialog-connect-provider").then((x) => {
dialog.show(() => <x.DialogConnectProvider provider={provider} directory={directory} />)
dialog.show(() => <x.DialogConnectProvider provider={provider} directory={directory} onBack={all} />)
})
}

View file

@ -1,5 +1,5 @@
import { type Accessor, Component, Show } from "solid-js"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { type Accessor, Component, Match, Show, Switch } from "solid-js"
import { createStore } from "solid-js/store"
import { popularProviders, useProviders } from "@/hooks/use-providers"
import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list"
@ -12,9 +12,13 @@ import { DialogCustomProvider } from "./dialog-custom-provider"
const CUSTOM_ID = "_custom"
export const DialogSelectProvider: Component<{ directory?: Accessor<string | undefined> }> = (props) => {
const dialog = useDialog()
const providers = useProviders(props.directory)
const language = useLanguage()
const [store, setStore] = createStore({ selected: undefined as string | undefined })
function showPicker() {
setStore("selected", undefined)
}
const popularGroup = () => language.t("dialog.provider.group.popular")
const otherGroup = () => language.t("dialog.provider.group.other")
@ -27,61 +31,67 @@ export const DialogSelectProvider: Component<{ directory?: Accessor<string | und
}
return (
<Dialog title={language.t("command.provider.connect")} transition>
<List
class="px-3"
search={{ placeholder: language.t("dialog.provider.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.provider.empty")}
activeIcon="plus-small"
key={(x) => x?.id}
items={() => {
language.locale()
return [{ id: CUSTOM_ID, name: customLabel() }, ...providers.all().values()]
}}
filterKeys={["id", "name"]}
groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())}
sortBy={(a, b) => {
if (a.id === CUSTOM_ID) return -1
if (b.id === CUSTOM_ID) return 1
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
return a.name.localeCompare(b.name)
}}
sortGroupsBy={(a, b) => {
const popular = popularGroup()
if (a.category === popular && b.category !== popular) return -1
if (b.category === popular && a.category !== popular) return 1
return 0
}}
onSelect={(x) => {
if (!x) return
if (x.id === CUSTOM_ID) {
dialog.show(() => <DialogCustomProvider back="providers" directory={props.directory} />)
return
}
dialog.show(() => <DialogConnectProvider provider={x.id} directory={props.directory} />)
}}
>
{(i) => (
<div class="px-1.25 w-full flex items-center gap-x-3">
<ProviderIcon data-slot="list-item-extra-icon" id={i.id} />
<span>{i.name}</span>
<Show when={i.id === "opencode"}>
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.opencode.tagline")}</div>
</Show>
<Show when={i.id === CUSTOM_ID}>
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
</Show>
<Show when={i.id === "opencode"}>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</Show>
<Show when={note(i.id)}>{(value) => <div class="text-14-regular text-text-weak">{value()}</div>}</Show>
<Show when={i.id === "opencode-go"}>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</Show>
</div>
)}
</List>
</Dialog>
<Switch>
<Match when={store.selected === CUSTOM_ID}>
<DialogCustomProvider onBack={showPicker} />
</Match>
<Match when={store.selected && store.selected !== CUSTOM_ID ? store.selected : undefined}>
{(provider) => <DialogConnectProvider provider={provider()} directory={props.directory} onBack={showPicker} />}
</Match>
<Match when={true}>
<Dialog class="h-full" transition title={language.t("command.provider.connect")}>
<List
class="px-3"
search={{ placeholder: language.t("dialog.provider.search.placeholder"), autofocus: true }}
emptyMessage={language.t("dialog.provider.empty")}
activeIcon="plus-small"
key={(x) => x?.id}
items={() => {
language.locale()
return [{ id: CUSTOM_ID, name: customLabel() }, ...providers.all().values()]
}}
filterKeys={["id", "name"]}
groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())}
sortBy={(a, b) => {
if (a.id === CUSTOM_ID) return -1
if (b.id === CUSTOM_ID) return 1
if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
return a.name.localeCompare(b.name)
}}
sortGroupsBy={(a, b) => {
const popular = popularGroup()
if (a.category === popular && b.category !== popular) return -1
if (b.category === popular && a.category !== popular) return 1
return 0
}}
onSelect={(x) => {
if (!x) return
setStore("selected", x.id)
}}
>
{(i) => (
<div class="px-1.25 w-full flex items-center gap-x-3">
<ProviderIcon data-slot="list-item-extra-icon" id={i.id} />
<span>{i.name}</span>
<Show when={i.id === "opencode"}>
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.opencode.tagline")}</div>
</Show>
<Show when={i.id === CUSTOM_ID}>
<Tag>{language.t("settings.providers.tag.custom")}</Tag>
</Show>
<Show when={i.id === "opencode"}>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</Show>
<Show when={note(i.id)}>{(value) => <div class="text-14-regular text-text-weak">{value()}</div>}</Show>
<Show when={i.id === "opencode-go"}>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</Show>
</div>
)}
</List>
</Dialog>
</Match>
</Switch>
)
}

View file

@ -211,7 +211,12 @@ const SettingsProvidersContent: Component = () => {
variant="secondary"
icon="plus-small"
onClick={() => {
dialog.show(() => <DialogConnectProvider provider={item.id} />)
dialog.show(() => (
<DialogConnectProvider
provider={item.id}
onBack={() => dialog.show(() => <DialogSelectProvider />)}
/>
))
}}
>
{language.t("common.connect")}
@ -239,7 +244,7 @@ const SettingsProvidersContent: Component = () => {
variant="secondary"
icon="plus-small"
onClick={() => {
dialog.show(() => <DialogCustomProvider back="close" />)
dialog.show(() => <DialogCustomProvider onBack={dialog.close} />)
}}
>
{language.t("common.connect")}

View file

@ -211,7 +211,12 @@ export const SettingsProvidersV2: Component = () => {
variant="neutral"
icon="plus"
onClick={() => {
dialog.show(() => <DialogConnectProvider provider={item.id} />)
dialog.show(() => (
<DialogConnectProvider
provider={item.id}
onBack={() => dialog.show(() => <DialogSelectProvider />)}
/>
))
}}
>
{language.t("common.connect")}
@ -241,7 +246,7 @@ export const SettingsProvidersV2: Component = () => {
variant="neutral"
icon="plus"
onClick={() => {
dialog.show(() => <DialogCustomProvider back="close" />)
dialog.show(() => <DialogCustomProvider onBack={dialog.close} />)
}}
>
{language.t("common.connect")}

View file

@ -77,7 +77,16 @@ export function useUsageExceededDialogs() {
if (dontShowAgain) setGoUpsellState(keys.dontShow, Date.now())
else {
void import("../../components/dialog-connect-provider").then((x) =>
dialog.show(() => <x.DialogConnectProvider provider="opencode-go" />),
dialog.show(() => (
<x.DialogConnectProvider
provider="opencode-go"
onBack={() => {
void import("../../components/dialog-select-provider").then((provider) => {
dialog.show(() => <provider.DialogSelectProvider />)
})
}}
/>
)),
)
}
}}