Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 9 additions & 17 deletions packages/app/src/components/dialog-connect-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down Expand Up @@ -364,19 +362,11 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc
}

function goBack() {
if (methods().length === 1) {
all()
return
}
if (store.authorization) {
dispatch({ type: "method.reset" })
return
}
if (store.methodIndex !== undefined) {
if (methods().length > 1 && store.methodIndex !== undefined) {
dispatch({ type: "method.reset" })
return
}
all()
props.onBack()
}

function MethodSelection() {
Expand Down Expand Up @@ -600,6 +590,7 @@ export function DialogConnectProvider(props: { provider: string; directory?: Acc

return (
<Dialog
class="h-full"
title={
<IconButton
tabIndex={-1}
Expand All @@ -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">
Expand Down
17 changes: 4 additions & 13 deletions packages/app/src/components/dialog-custom-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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",
Expand Down Expand Up @@ -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")}
/>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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} />)
})
}

Expand Down
128 changes: 69 additions & 59 deletions packages/app/src/components/dialog-select-provider.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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")
Expand All @@ -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>
)
}
9 changes: 7 additions & 2 deletions packages/app/src/components/settings-providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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")}
Expand Down Expand Up @@ -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")}
Expand Down
9 changes: 7 additions & 2 deletions packages/app/src/components/settings-v2/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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")}
Expand Down Expand Up @@ -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")}
Expand Down
11 changes: 10 additions & 1 deletion packages/app/src/pages/session/usage-exceeded-dialogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 />)
})
}}
/>
)),
)
}
}}
Expand Down
Loading