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
co-authored by Jay Brendan Allan
parent e12cb7fb6b
commit 14df88eab5
7 changed files with 107 additions and 95 deletions
@@ -17,19 +17,17 @@ import { useServerSync } from "@/context/server-sync"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { useProviders } from "@/hooks/use-providers" 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 dialog = useDialog()
const serverSync = useServerSync() const serverSync = useServerSync()
const serverSDK = useServerSDK() const serverSDK = useServerSDK()
const language = useLanguage() const language = useLanguage()
const providers = useProviders(props.directory) 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 alive = { value: true }
const timer = { current: undefined as ReturnType<typeof setTimeout> | undefined } const timer = { current: undefined as ReturnType<typeof setTimeout> | undefined }
@@ -364,19 +362,11 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
} }
function goBack() { function goBack() {
if (methods().length === 1) { if (methods().length > 1 && store.methodIndex !== undefined) {
all()
return
}
if (store.authorization) {
dispatch({ type: "method.reset" }) dispatch({ type: "method.reset" })
return return
} }
if (store.methodIndex !== undefined) { props.onBack()
dispatch({ type: "method.reset" })
return
}
all()
} }
function MethodSelection() { function MethodSelection() {
@@ -600,6 +590,7 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
return ( return (
<Dialog <Dialog
class="h-full"
title={ title={
<IconButton <IconButton
tabIndex={-1} tabIndex={-1}
@@ -609,6 +600,7 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
aria-label={language.t("common.goBack")} aria-label={language.t("common.goBack")}
/> />
} }
transition
> >
<div class="flex flex-col gap-6 px-2.5 pb-3"> <div class="flex flex-col gap-6 px-2.5 pb-3">
<div class="px-2.5 flex gap-4 items-center"> <div class="px-2.5 flex gap-4 items-center">
@@ -6,18 +6,16 @@ import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { useMutation } from "@tanstack/solid-query" import { useMutation } from "@tanstack/solid-query"
import { TextField } from "@opencode-ai/ui/text-field" import { TextField } from "@opencode-ai/ui/text-field"
import { showToast } from "@/utils/toast" 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 { createStore, produce } from "solid-js/store"
import { Link } from "@/components/link" import { Link } from "@/components/link"
import { useServerSDK } from "@/context/server-sdk" import { useServerSDK } from "@/context/server-sdk"
import { useServerSync } from "@/context/server-sync" import { useServerSync } from "@/context/server-sync"
import { useLanguage } from "@/context/language" import { useLanguage } from "@/context/language"
import { type FormState, headerRow, modelRow, validateCustomProvider } from "./dialog-custom-provider-form" import { type FormState, headerRow, modelRow, validateCustomProvider } from "./dialog-custom-provider-form"
import { DialogSelectProvider } from "./dialog-select-provider"
type Props = { type Props = {
back?: "providers" | "close" onBack: () => void
directory?: Accessor<string | undefined>
} }
export function DialogCustomProvider(props: Props) { export function DialogCustomProvider(props: Props) {
@@ -36,14 +34,6 @@ export function DialogCustomProvider(props: Props) {
err: {}, err: {},
}) })
const goBack = () => {
if (props.back === "close") {
dialog.close()
return
}
dialog.show(() => <DialogSelectProvider directory={props.directory} />)
}
const addModel = () => { const addModel = () => {
setForm( setForm(
"models", "models",
@@ -164,12 +154,13 @@ export function DialogCustomProvider(props: Props) {
return ( return (
<Dialog <Dialog
class="h-full"
title={ title={
<IconButton <IconButton
tabIndex={-1} tabIndex={-1}
icon="arrow-left" icon="arrow-left"
variant="ghost" variant="ghost"
onClick={goBack} onClick={props.onBack}
aria-label={language.t("common.goBack")} aria-label={language.t("common.goBack")}
/> />
} }
@@ -24,7 +24,7 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
const connect = (provider: string) => { const connect = (provider: string) => {
void import("./dialog-connect-provider").then((x) => { 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} />)
}) })
} }
@@ -1,5 +1,5 @@
import { type Accessor, Component, Show } from "solid-js" import { type Accessor, Component, Match, Show, Switch } from "solid-js"
import { useDialog } from "@opencode-ai/ui/context/dialog" import { createStore } from "solid-js/store"
import { popularProviders, useProviders } from "@/hooks/use-providers" import { popularProviders, useProviders } from "@/hooks/use-providers"
import { Dialog } from "@opencode-ai/ui/dialog" import { Dialog } from "@opencode-ai/ui/dialog"
import { List } from "@opencode-ai/ui/list" import { List } from "@opencode-ai/ui/list"
@@ -12,9 +12,13 @@ import { DialogCustomProvider } from "./dialog-custom-provider"
const CUSTOM_ID = "_custom" const CUSTOM_ID = "_custom"
export const DialogSelectProvider: Component<{ directory?: Accessor<string | undefined> }> = (props) => { export const DialogSelectProvider: Component<{ directory?: Accessor<string | undefined> }> = (props) => {
const dialog = useDialog()
const providers = useProviders(props.directory) const providers = useProviders(props.directory)
const language = useLanguage() 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 popularGroup = () => language.t("dialog.provider.group.popular")
const otherGroup = () => language.t("dialog.provider.group.other") const otherGroup = () => language.t("dialog.provider.group.other")
@@ -27,61 +31,67 @@ export const DialogSelectProvider: Component<{ directory?: Accessor<string | und
} }
return ( return (
<Dialog title={language.t("command.provider.connect")} transition> <Switch>
<List <Match when={store.selected === CUSTOM_ID}>
class="px-3" <DialogCustomProvider onBack={showPicker} />
search={{ placeholder: language.t("dialog.provider.search.placeholder"), autofocus: true }} </Match>
emptyMessage={language.t("dialog.provider.empty")} <Match when={store.selected && store.selected !== CUSTOM_ID ? store.selected : undefined}>
activeIcon="plus-small" {(provider) => <DialogConnectProvider provider={provider()} directory={props.directory} onBack={showPicker} />}
key={(x) => x?.id} </Match>
items={() => { <Match when={true}>
language.locale() <Dialog class="h-full" transition title={language.t("command.provider.connect")}>
return [{ id: CUSTOM_ID, name: customLabel() }, ...providers.all().values()] <List
}} class="px-3"
filterKeys={["id", "name"]} search={{ placeholder: language.t("dialog.provider.search.placeholder"), autofocus: true }}
groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())} emptyMessage={language.t("dialog.provider.empty")}
sortBy={(a, b) => { activeIcon="plus-small"
if (a.id === CUSTOM_ID) return -1 key={(x) => x?.id}
if (b.id === CUSTOM_ID) return 1 items={() => {
if (popularProviders.includes(a.id) && popularProviders.includes(b.id)) language.locale()
return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id) return [{ id: CUSTOM_ID, name: customLabel() }, ...providers.all().values()]
return a.name.localeCompare(b.name) }}
}} filterKeys={["id", "name"]}
sortGroupsBy={(a, b) => { groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())}
const popular = popularGroup() sortBy={(a, b) => {
if (a.category === popular && b.category !== popular) return -1 if (a.id === CUSTOM_ID) return -1
if (b.category === popular && a.category !== popular) return 1 if (b.id === CUSTOM_ID) return 1
return 0 if (popularProviders.includes(a.id) && popularProviders.includes(b.id))
}} return popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)
onSelect={(x) => { return a.name.localeCompare(b.name)
if (!x) return }}
if (x.id === CUSTOM_ID) { sortGroupsBy={(a, b) => {
dialog.show(() => <DialogCustomProvider back="providers" directory={props.directory} />) const popular = popularGroup()
return if (a.category === popular && b.category !== popular) return -1
} if (b.category === popular && a.category !== popular) return 1
dialog.show(() => <DialogConnectProvider provider={x.id} directory={props.directory} />) return 0
}} }}
> onSelect={(x) => {
{(i) => ( if (!x) return
<div class="px-1.25 w-full flex items-center gap-x-3"> setStore("selected", x.id)
<ProviderIcon data-slot="list-item-extra-icon" id={i.id} /> }}
<span>{i.name}</span> >
<Show when={i.id === "opencode"}> {(i) => (
<div class="text-14-regular text-text-weak">{language.t("dialog.provider.opencode.tagline")}</div> <div class="px-1.25 w-full flex items-center gap-x-3">
</Show> <ProviderIcon data-slot="list-item-extra-icon" id={i.id} />
<Show when={i.id === CUSTOM_ID}> <span>{i.name}</span>
<Tag>{language.t("settings.providers.tag.custom")}</Tag> <Show when={i.id === "opencode"}>
</Show> <div class="text-14-regular text-text-weak">{language.t("dialog.provider.opencode.tagline")}</div>
<Show when={i.id === "opencode"}> </Show>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag> <Show when={i.id === CUSTOM_ID}>
</Show> <Tag>{language.t("settings.providers.tag.custom")}</Tag>
<Show when={note(i.id)}>{(value) => <div class="text-14-regular text-text-weak">{value()}</div>}</Show> </Show>
<Show when={i.id === "opencode-go"}> <Show when={i.id === "opencode"}>
<Tag>{language.t("dialog.provider.tag.recommended")}</Tag> <Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</Show> </Show>
</div> <Show when={note(i.id)}>{(value) => <div class="text-14-regular text-text-weak">{value()}</div>}</Show>
)} <Show when={i.id === "opencode-go"}>
</List> <Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
</Dialog> </Show>
</div>
)}
</List>
</Dialog>
</Match>
</Switch>
) )
} }
@@ -211,7 +211,12 @@ const SettingsProvidersContent: Component = () => {
variant="secondary" variant="secondary"
icon="plus-small" icon="plus-small"
onClick={() => { onClick={() => {
dialog.show(() => <DialogConnectProvider provider={item.id} />) dialog.show(() => (
<DialogConnectProvider
provider={item.id}
onBack={() => dialog.show(() => <DialogSelectProvider />)}
/>
))
}} }}
> >
{language.t("common.connect")} {language.t("common.connect")}
@@ -239,7 +244,7 @@ const SettingsProvidersContent: Component = () => {
variant="secondary" variant="secondary"
icon="plus-small" icon="plus-small"
onClick={() => { onClick={() => {
dialog.show(() => <DialogCustomProvider back="close" />) dialog.show(() => <DialogCustomProvider onBack={dialog.close} />)
}} }}
> >
{language.t("common.connect")} {language.t("common.connect")}
@@ -211,7 +211,12 @@ export const SettingsProvidersV2: Component = () => {
variant="neutral" variant="neutral"
icon="plus" icon="plus"
onClick={() => { onClick={() => {
dialog.show(() => <DialogConnectProvider provider={item.id} />) dialog.show(() => (
<DialogConnectProvider
provider={item.id}
onBack={() => dialog.show(() => <DialogSelectProvider />)}
/>
))
}} }}
> >
{language.t("common.connect")} {language.t("common.connect")}
@@ -241,7 +246,7 @@ export const SettingsProvidersV2: Component = () => {
variant="neutral" variant="neutral"
icon="plus" icon="plus"
onClick={() => { onClick={() => {
dialog.show(() => <DialogCustomProvider back="close" />) dialog.show(() => <DialogCustomProvider onBack={dialog.close} />)
}} }}
> >
{language.t("common.connect")} {language.t("common.connect")}
@@ -77,7 +77,16 @@ export function useUsageExceededDialogs() {
if (dontShowAgain) setGoUpsellState(keys.dontShow, Date.now()) if (dontShowAgain) setGoUpsellState(keys.dontShow, Date.now())
else { else {
void import("../../components/dialog-connect-provider").then((x) => 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 />)
})
}}
/>
)),
) )
} }
}} }}