diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index c94cca50535..6499642b389 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -7,20 +7,167 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { List, type ListRef } from "@opencode-ai/ui/list" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { Spinner } from "@opencode-ai/ui/spinner" +import { Tag } from "@opencode-ai/ui/tag" import { TextField } from "@opencode-ai/ui/text-field" import { showToast } from "@/utils/toast" -import { type Accessor, createEffect, createMemo, createResource, Match, onCleanup, onMount, Switch } from "solid-js" +import { + type Accessor, + type Component, + createEffect, + createMemo, + createResource, + Match, + onCleanup, + onMount, + Show, + Switch, +} 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 { useProviders } from "@/hooks/use-providers" +import { popularProviders, useProviders } from "@/hooks/use-providers" +import { CustomProviderForm } from "./dialog-custom-provider" -export function DialogConnectProvider(props: { +const CUSTOM_ID = "_custom" + +export function useProviderConnectController(options: { onBack?: () => void } = {}) { + const [store, setStore] = createStore({ selected: undefined as string | undefined }) + const reset = () => setStore("selected", undefined) + + return { + selected: () => store.selected, + select: (provider?: string) => setStore("selected", provider), + back: options.onBack ?? reset, + } +} + +export const DialogConnectProvider: Component<{ + directory?: Accessor + controller?: ReturnType +}> = (props) => { + const fallback = useProviderConnectController() + const controller = props.controller ?? fallback + const language = useLanguage() + const reset = controller.back + const back = { current: reset } + const select = (provider?: string) => { + back.current = reset + controller.select(provider) + } + + return ( + + back.current()} + aria-label={language.t("common.goBack")} + /> + + } + > + + + + + + {(provider) => ( + (back.current = handler)} + /> + )} + + + + + + + ) +} + +function ProviderPicker(props: { directory?: Accessor; onSelect: (provider: string) => void }) { + const providers = useProviders(props.directory) + const language = useLanguage() + const popularGroup = () => language.t("dialog.provider.group.popular") + const otherGroup = () => language.t("dialog.provider.group.other") + const customLabel = () => language.t("settings.providers.tag.custom") + const note = (id: string) => { + if (id === "anthropic") return language.t("dialog.provider.anthropic.note") + if (id === "openai") return language.t("dialog.provider.openai.note") + if (id.startsWith("github-copilot")) return language.t("dialog.provider.copilot.note") + if (id === "opencode-go") return language.t("dialog.provider.opencodeGo.tagline") + return undefined + } + + return ( + 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 + props.onSelect(x.id) + }} + > + {(i) => ( +
+ + {i.name} + +
{language.t("dialog.provider.opencode.tagline")}
+
+ + {language.t("settings.providers.tag.custom")} + + + {language.t("dialog.provider.tag.recommended")} + + {(value) =>
{value()}
}
+ + {language.t("dialog.provider.tag.recommended")} + +
+ )} +
+ ) +} + +function ProviderConnection(props: { provider: string directory?: Accessor onBack: () => void + setBack: (handler: () => void) => void }) { const dialog = useDialog() const serverSync = useServerSync() @@ -369,6 +516,8 @@ export function DialogConnectProvider(props: { props.onBack() } + props.setBack(goBack) + function MethodSelection() { return ( <> @@ -589,81 +738,67 @@ export function DialogConnectProvider(props: { } return ( - - } - transition - > -
-
- -
- - - {language.t("provider.connect.title.anthropicProMax")} - - {language.t("provider.connect.title", { provider: provider().name })} - -
-
-
-
- - -
-
- - {language.t("provider.connect.status.inProgress")} -
-
-
- - - - -
-
- - {language.t("provider.connect.status.inProgress")} -
-
-
- - - - -
-
- - {language.t("provider.connect.status.failed", { error: store.error ?? "" })} -
-
-
- - - - - - - - - - - - - -
-
+
+
+ +
+ + + {language.t("provider.connect.title.anthropicProMax")} + + {language.t("provider.connect.title", { provider: provider().name })} +
-
+
+
+ + +
+
+ + {language.t("provider.connect.status.inProgress")} +
+
+
+ + + + +
+
+ + {language.t("provider.connect.status.inProgress")} +
+
+
+ + + + +
+
+ + {language.t("provider.connect.status.failed", { error: store.error ?? "" })} +
+
+
+ + + + + + + + + + + + + +
+
+
+ ) } diff --git a/packages/app/src/components/dialog-custom-provider.tsx b/packages/app/src/components/dialog-custom-provider.tsx index e8cae859e06..363a2e390a4 100644 --- a/packages/app/src/components/dialog-custom-provider.tsx +++ b/packages/app/src/components/dialog-custom-provider.tsx @@ -19,6 +19,28 @@ type Props = { } export function DialogCustomProvider(props: Props) { + const language = useLanguage() + + return ( + + } + transition + > + + + ) +} + +export function CustomProviderForm() { const dialog = useDialog() const serverSync = useServerSync() const serverSDK = useServerSDK() @@ -153,169 +175,155 @@ export function DialogCustomProvider(props: Props) { } return ( - - } - transition - > -
-
- -
{language.t("provider.custom.title")}
+
+
+ +
{language.t("provider.custom.title")}
+
+ +
+

+ {language.t("provider.custom.description.prefix")} + + {language.t("provider.custom.description.link")} + + {language.t("provider.custom.description.suffix")} +

+ +
+ setField("providerID", v)} + validationState={form.err.providerID ? "invalid" : undefined} + error={form.err.providerID} + /> + setField("name", v)} + validationState={form.err.name ? "invalid" : undefined} + error={form.err.name} + /> + setField("baseURL", v)} + validationState={form.err.baseURL ? "invalid" : undefined} + error={form.err.baseURL} + /> + setField("apiKey", v)} + />
- -

- {language.t("provider.custom.description.prefix")} - - {language.t("provider.custom.description.link")} - - {language.t("provider.custom.description.suffix")} -

- -
- setField("providerID", v)} - validationState={form.err.providerID ? "invalid" : undefined} - error={form.err.providerID} - /> - setField("name", v)} - validationState={form.err.name ? "invalid" : undefined} - error={form.err.name} - /> - setField("baseURL", v)} - validationState={form.err.baseURL ? "invalid" : undefined} - error={form.err.baseURL} - /> - setField("apiKey", v)} - /> -
- -
- - - {(m, i) => ( -
-
- setModel(i(), "id", v)} - validationState={m.err.id ? "invalid" : undefined} - error={m.err.id} - /> -
-
- setModel(i(), "name", v)} - validationState={m.err.name ? "invalid" : undefined} - error={m.err.name} - /> -
- removeModel(i())} - disabled={form.models.length <= 1} - aria-label={language.t("provider.custom.models.remove")} +
+ + + {(m, i) => ( +
+
+ setModel(i(), "id", v)} + validationState={m.err.id ? "invalid" : undefined} + error={m.err.id} />
- )} - - -
- -
- - - {(h, i) => ( -
-
- setHeader(i(), "key", v)} - validationState={h.err.key ? "invalid" : undefined} - error={h.err.key} - /> -
-
- setHeader(i(), "value", v)} - validationState={h.err.value ? "invalid" : undefined} - error={h.err.value} - /> -
- removeHeader(i())} - disabled={form.headers.length <= 1} - aria-label={language.t("provider.custom.headers.remove")} +
+ setModel(i(), "name", v)} + validationState={m.err.name ? "invalid" : undefined} + error={m.err.name} />
- )} - - -
- -
+ )} +
+ - -
-
+ + +
+ + + {(h, i) => ( +
+
+ setHeader(i(), "key", v)} + validationState={h.err.key ? "invalid" : undefined} + error={h.err.key} + /> +
+
+ setHeader(i(), "value", v)} + validationState={h.err.value ? "invalid" : undefined} + error={h.err.value} + /> +
+ removeHeader(i())} + disabled={form.headers.length <= 1} + aria-label={language.t("provider.custom.headers.remove")} + /> +
+ )} +
+ +
+ + + + ) } diff --git a/packages/app/src/components/dialog-manage-models.tsx b/packages/app/src/components/dialog-manage-models.tsx index 0238f543c41..d6c5e991867 100644 --- a/packages/app/src/components/dialog-manage-models.tsx +++ b/packages/app/src/components/dialog-manage-models.tsx @@ -16,7 +16,7 @@ import { useLocal } from "@/context/local" import { popularProviders } from "@/hooks/use-providers" import { useLanguage } from "@/context/language" import { useDialog } from "@opencode-ai/ui/context/dialog" -import { DialogSelectProvider } from "./dialog-select-provider" +import { DialogConnectProvider } from "./dialog-connect-provider" import { decode64 } from "@/utils/base64" import { SettingsListV2 } from "./settings-v2/parts/list" import { SettingsRowV2 } from "./settings-v2/parts/row" @@ -31,7 +31,7 @@ export const DialogManageModels: Component = () => { const directory = () => decode64(local.slug()) const handleConnectProvider = () => { - dialog.show(() => ) + void dialog.show(() => ) } const providerRank = (id: string) => popularProviders.indexOf(id) const providerList = (providerID: string) => local.model.list().filter((x) => x.provider.id === providerID) @@ -123,7 +123,7 @@ export const DialogManageModelsV2: Component = () => { const directory = () => decode64(local.slug()) const handleConnectProvider = () => { - dialog.show(() => ) + void dialog.show(() => ) } const providerList = (providerID: string) => local.model.list().filter((x) => x.provider.id === providerID) const providerVisible = (providerID: string) => diff --git a/packages/app/src/components/dialog-select-model-unpaid.tsx b/packages/app/src/components/dialog-select-model-unpaid.tsx index 0a32ad48b36..4611a36c950 100644 --- a/packages/app/src/components/dialog-select-model-unpaid.tsx +++ b/packages/app/src/components/dialog-select-model-unpaid.tsx @@ -22,17 +22,16 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props const providers = useProviders(directory) const language = useLanguage() - const connect = (provider: string) => { + const openProviders = (provider?: string) => { void import("./dialog-connect-provider").then((x) => { - dialog.show(() => ) + const controller = x.useProviderConnectController() + controller.select(provider) + void dialog.show(() => ) }) } - const all = () => { - void import("./dialog-select-provider").then((x) => { - dialog.show(() => ) - }) - } + const connect = (provider: string) => openProviders(provider) + const all = () => openProviders() let listRef: ListRef | undefined const handleKeyDown = (e: KeyboardEvent) => { diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index f026f371eac..cea5f34d0d9 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -155,8 +155,8 @@ export function ModelSelectorPopover(props: { const handleConnectProvider = () => { close("provider") - void import("./dialog-select-provider").then((x) => { - dialog.show(() => ) + void import("./dialog-connect-provider").then((x) => { + void dialog.show(() => ) }) } const language = useLanguage() @@ -503,8 +503,8 @@ export const DialogSelectModel: Component<{ provider?: string; model?: ModelStat const directory = () => decode64(local.slug()) const provider = () => { - void import("./dialog-select-provider").then((x) => { - dialog.show(() => ) + void import("./dialog-connect-provider").then((x) => { + void dialog.show(() => ) }) } diff --git a/packages/app/src/components/dialog-select-provider.tsx b/packages/app/src/components/dialog-select-provider.tsx deleted file mode 100644 index e1facf48219..00000000000 --- a/packages/app/src/components/dialog-select-provider.tsx +++ /dev/null @@ -1,97 +0,0 @@ -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" -import { Tag } from "@opencode-ai/ui/tag" -import { ProviderIcon } from "@opencode-ai/ui/provider-icon" -import { DialogConnectProvider } from "./dialog-connect-provider" -import { useLanguage } from "@/context/language" -import { DialogCustomProvider } from "./dialog-custom-provider" - -const CUSTOM_ID = "_custom" - -export const DialogSelectProvider: Component<{ directory?: Accessor }> = (props) => { - 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") - const customLabel = () => language.t("settings.providers.tag.custom") - const note = (id: string) => { - if (id === "anthropic") return language.t("dialog.provider.anthropic.note") - if (id === "openai") return language.t("dialog.provider.openai.note") - if (id.startsWith("github-copilot")) return language.t("dialog.provider.copilot.note") - if (id === "opencode-go") return language.t("dialog.provider.opencodeGo.tagline") - } - - return ( - - - - - - {(provider) => } - - - - 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) => ( -
- - {i.name} - -
{language.t("dialog.provider.opencode.tagline")}
-
- - {language.t("settings.providers.tag.custom")} - - - {language.t("dialog.provider.tag.recommended")} - - {(value) =>
{value()}
}
- - {language.t("dialog.provider.tag.recommended")} - -
- )} -
-
-
-
- ) -} diff --git a/packages/app/src/components/dialog-settings.tsx b/packages/app/src/components/dialog-settings.tsx index 20d71f4bfd4..231237eb8b9 100644 --- a/packages/app/src/components/dialog-settings.tsx +++ b/packages/app/src/components/dialog-settings.tsx @@ -4,19 +4,30 @@ import { Tabs } from "@opencode-ai/ui/tabs" import { Icon } from "@opencode-ai/ui/icon" import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" +import { useDialog } from "@opencode-ai/ui/context/dialog" import { SettingsGeneral } from "./settings-general" import { SettingsKeybinds } from "./settings-keybinds" import { SettingsProviders } from "./settings-providers" import { SettingsModels } from "./settings-models" import { SettingsServers } from "./settings-servers" -export const DialogSettings: Component = () => { +export const DialogSettings: Component<{ defaultValue?: string }> = (props) => { const language = useLanguage() const platform = usePlatform() + const dialog = useDialog() + + const showProviders = () => { + void dialog.show(() => ) + } return ( - +
@@ -70,7 +81,7 @@ export const DialogSettings: Component = () => { - + diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index 9791fc21fca..bcd30edbc7d 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -8,8 +8,7 @@ import { createMemo, type Component, For, Show } from "solid-js" import { useLanguage } from "@/context/language" import { useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" -import { DialogConnectProvider } from "./dialog-connect-provider" -import { DialogSelectProvider } from "./dialog-select-provider" +import { DialogConnectProvider, useProviderConnectController } from "./dialog-connect-provider" import { DialogCustomProvider } from "./dialog-custom-provider" import { SettingsList } from "./settings-list" import { SettingsServerPicker, SettingsServerScope } from "./settings-server-picker" @@ -28,20 +27,26 @@ const PROVIDER_NOTES = [ { match: (id: string) => id === "vercel", key: "dialog.provider.vercel.note" }, ] as const -export const SettingsProviders: Component = () => { +export const SettingsProviders: Component<{ onBack?: () => void }> = (props) => { return ( - + ) } -const SettingsProvidersContent: Component = () => { +const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) => { const dialog = useDialog() const language = useLanguage() const serverSDK = useServerSDK() const serverSync = useServerSync() const providers = useProviders() + const providerConnect = useProviderConnectController({ onBack: props.onBack }) + + const connect = (provider?: string) => { + providerConnect.select(provider) + void dialog.show(() => ) + } const connected = createMemo(() => { return providers @@ -206,19 +211,7 @@ const SettingsProvidersContent: Component = () => { {(key) => {language.t(key())}}
-
@@ -255,9 +248,7 @@ const SettingsProvidersContent: Component = () => { diff --git a/packages/app/src/components/settings-v2/dialog-settings-v2.tsx b/packages/app/src/components/settings-v2/dialog-settings-v2.tsx index a93a10dcb2f..aee4fc9896f 100644 --- a/packages/app/src/components/settings-v2/dialog-settings-v2.tsx +++ b/packages/app/src/components/settings-v2/dialog-settings-v2.tsx @@ -10,16 +10,28 @@ import { SettingsProvidersV2 } from "./providers" import { SettingsModelsV2 } from "./models" import "./settings-v2.css" import { SettingsServersV2 } from "./servers" +import { useDialog } from "@opencode-ai/ui/context/dialog" export const DialogSettings: Component<{ sessionID?: string + defaultValue?: string }> = (props) => { const language = useLanguage() const platform = usePlatform() + const dialog = useDialog() + + const showProviders = () => { + void dialog.show(() => ) + } return ( - +
@@ -73,7 +85,7 @@ export const DialogSettings: Component<{ - + diff --git a/packages/app/src/components/settings-v2/providers.tsx b/packages/app/src/components/settings-v2/providers.tsx index 99a4101f9a3..f945fa33c64 100644 --- a/packages/app/src/components/settings-v2/providers.tsx +++ b/packages/app/src/components/settings-v2/providers.tsx @@ -8,8 +8,7 @@ import { createMemo, type Component, For, Show } from "solid-js" import { useLanguage } from "@/context/language" import { useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" -import { DialogConnectProvider } from "../dialog-connect-provider" -import { DialogSelectProvider } from "../dialog-select-provider" +import { DialogConnectProvider, useProviderConnectController } from "../dialog-connect-provider" import { DialogCustomProvider } from "../dialog-custom-provider" import { SettingsListV2 } from "./parts/list" import "./settings-v2.css" @@ -30,12 +29,18 @@ const PROVIDER_NOTES = [ const PROVIDER_ICON_SIZE = 16 -export const SettingsProvidersV2: Component = () => { +export const SettingsProvidersV2: Component<{ onBack?: () => void }> = (props) => { const dialog = useDialog() const language = useLanguage() const serverSdk = useServerSDK() const serverSync = useServerSync() const providers = useProviders() + const providerConnect = useProviderConnectController({ onBack: props.onBack }) + + const connect = (provider?: string) => { + providerConnect.select(provider) + void dialog.show(() => ) + } const connected = createMemo(() => { return providers @@ -206,19 +211,7 @@ export const SettingsProvidersV2: Component = () => {
- { - dialog.show(() => ( - dialog.show(() => )} - /> - )) - }} - > + connect(item.id)}> {language.t("common.connect")} @@ -254,13 +247,7 @@ export const SettingsProvidersV2: Component = () => { - diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 690460d3af6..fd9d16b90ad 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1095,9 +1095,9 @@ export default function LegacyLayout(props: ParentProps) { function connectProvider() { const run = ++dialogRun - void import("@/components/dialog-select-provider").then((x) => { + void import("@/components/dialog-connect-provider").then((x) => { if (dialogDead || dialogRun !== run) return - dialog.show(() => ) + void dialog.show(() => ) }) } diff --git a/packages/app/src/pages/session/usage-exceeded-dialogs.tsx b/packages/app/src/pages/session/usage-exceeded-dialogs.tsx index 5c77e7bcecc..d56fa3d1f48 100644 --- a/packages/app/src/pages/session/usage-exceeded-dialogs.tsx +++ b/packages/app/src/pages/session/usage-exceeded-dialogs.tsx @@ -76,18 +76,11 @@ export function useUsageExceededDialogs() { setGoUpsellState(keys.lastSeenAt, Date.now()) if (dontShowAgain) setGoUpsellState(keys.dontShow, Date.now()) else { - void import("../../components/dialog-connect-provider").then((x) => - dialog.show(() => ( - { - void import("../../components/dialog-select-provider").then((provider) => { - dialog.show(() => ) - }) - }} - /> - )), - ) + void import("../../components/dialog-connect-provider").then((x) => { + const controller = x.useProviderConnectController() + controller.select("opencode-go") + void dialog.show(() => ) + }) } }} />