summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src
diff options
context:
space:
mode:
authoradamelmore <[email protected]>2026-01-25 15:59:56 -0600
committerAdam <[email protected]>2026-01-25 20:40:00 -0600
commit03d884797c19e7cbe92d7ef237c4b28d51b58f18 (patch)
tree3bea0b88823ce6ef0a6f83cff90c3a14b9432610 /packages/app/src
parenta5b72a7d994618555467b4269f48b0c000e2db84 (diff)
downloadopencode-03d884797c19e7cbe92d7ef237c4b28d51b58f18.tar.gz
opencode-03d884797c19e7cbe92d7ef237c4b28d51b58f18.zip
wip(app): provider settings
Diffstat (limited to 'packages/app/src')
-rw-r--r--packages/app/src/components/dialog-settings.tsx68
-rw-r--r--packages/app/src/components/settings-providers.tsx149
-rw-r--r--packages/app/src/i18n/en.ts10
3 files changed, 181 insertions, 46 deletions
diff --git a/packages/app/src/components/dialog-settings.tsx b/packages/app/src/components/dialog-settings.tsx
index dbbc8fa7a..0b8d108d0 100644
--- a/packages/app/src/components/dialog-settings.tsx
+++ b/packages/app/src/components/dialog-settings.tsx
@@ -39,16 +39,30 @@ export const DialogSettings: Component = () => {
"padding-top": "12px",
}}
>
- <Tabs.SectionTitle>{language.t("settings.section.desktop")}</Tabs.SectionTitle>
- <div style={{ display: "flex", "flex-direction": "column", gap: "6px", width: "100%" }}>
- <Tabs.Trigger value="general">
- <Icon name="sliders" />
- {language.t("settings.tab.general")}
- </Tabs.Trigger>
- <Tabs.Trigger value="shortcuts">
- <Icon name="keyboard" />
- {language.t("settings.tab.shortcuts")}
- </Tabs.Trigger>
+ <div style={{ display: "flex", "flex-direction": "column", gap: "12px" }}>
+ <div style={{ display: "flex", "flex-direction": "column", gap: "6px" }}>
+ <Tabs.SectionTitle>{language.t("settings.section.desktop")}</Tabs.SectionTitle>
+ <div style={{ display: "flex", "flex-direction": "column", gap: "6px", width: "100%" }}>
+ <Tabs.Trigger value="general">
+ <Icon name="sliders" />
+ {language.t("settings.tab.general")}
+ </Tabs.Trigger>
+ <Tabs.Trigger value="shortcuts">
+ <Icon name="keyboard" />
+ {language.t("settings.tab.shortcuts")}
+ </Tabs.Trigger>
+ </div>
+ </div>
+
+ <div style={{ display: "flex", "flex-direction": "column", gap: "6px" }}>
+ <Tabs.SectionTitle>{language.t("settings.section.server")}</Tabs.SectionTitle>
+ <div style={{ display: "flex", "flex-direction": "column", gap: "6px", width: "100%" }}>
+ <Tabs.Trigger value="providers">
+ <Icon name="server" />
+ {language.t("settings.providers.title")}
+ </Tabs.Trigger>
+ </div>
+ </div>
</div>
</div>
<div class="flex flex-col gap-1 pl-1 py-1 text-12-medium text-text-weak">
@@ -56,31 +70,6 @@ export const DialogSettings: Component = () => {
<span class="text-11-regular">v{platform.version}</span>
</div>
</div>
- {/* <Tabs.SectionTitle>Server</Tabs.SectionTitle> */}
- {/* <Tabs.Trigger value="permissions"> */}
- {/* <Icon name="checklist" /> */}
- {/* Permissions */}
- {/* </Tabs.Trigger> */}
- {/* <Tabs.Trigger value="providers"> */}
- {/* <Icon name="server" /> */}
- {/* Providers */}
- {/* </Tabs.Trigger> */}
- {/* <Tabs.Trigger value="models"> */}
- {/* <Icon name="brain" /> */}
- {/* Models */}
- {/* </Tabs.Trigger> */}
- {/* <Tabs.Trigger value="agents"> */}
- {/* <Icon name="task" /> */}
- {/* Agents */}
- {/* </Tabs.Trigger> */}
- {/* <Tabs.Trigger value="commands"> */}
- {/* <Icon name="console" /> */}
- {/* Commands */}
- {/* </Tabs.Trigger> */}
- {/* <Tabs.Trigger value="mcp"> */}
- {/* <Icon name="mcp" /> */}
- {/* MCP */}
- {/* </Tabs.Trigger> */}
</Tabs.List>
<Tabs.Content value="general" class="no-scrollbar">
<SettingsGeneral />
@@ -88,12 +77,9 @@ export const DialogSettings: Component = () => {
<Tabs.Content value="shortcuts" class="no-scrollbar">
<SettingsKeybinds />
</Tabs.Content>
- {/* <Tabs.Content value="permissions" class="no-scrollbar"> */}
- {/* <SettingsPermissions /> */}
- {/* </Tabs.Content> */}
- {/* <Tabs.Content value="providers" class="no-scrollbar"> */}
- {/* <SettingsProviders /> */}
- {/* </Tabs.Content> */}
+ <Tabs.Content value="providers" class="no-scrollbar">
+ <SettingsProviders />
+ </Tabs.Content>
{/* <Tabs.Content value="models" class="no-scrollbar"> */}
{/* <SettingsModels /> */}
{/* </Tabs.Content> */}
diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx
index 7b6ca1939..b175a570b 100644
--- a/packages/app/src/components/settings-providers.tsx
+++ b/packages/app/src/components/settings-providers.tsx
@@ -1,14 +1,153 @@
-import { Component } from "solid-js"
+import { Button } from "@opencode-ai/ui/button"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
+import { Tag } from "@opencode-ai/ui/tag"
+import { showToast } from "@opencode-ai/ui/toast"
+import type { IconName } from "@opencode-ai/ui/icons/provider"
+import { popularProviders, useProviders } from "@/hooks/use-providers"
+import { createMemo, type Component, For, Show } from "solid-js"
import { useLanguage } from "@/context/language"
+import { useGlobalSDK } from "@/context/global-sdk"
+import { DialogConnectProvider } from "./dialog-connect-provider"
+import { DialogSelectProvider } from "./dialog-select-provider"
+
+type ProviderSource = "env" | "api" | "config" | "custom"
+type ProviderMeta = { source?: ProviderSource }
export const SettingsProviders: Component = () => {
+ const dialog = useDialog()
const language = useLanguage()
+ const globalSDK = useGlobalSDK()
+ const providers = useProviders()
+
+ const connected = createMemo(() => providers.connected())
+ const popular = createMemo(() => {
+ const items = providers.popular().slice()
+ items.sort((a, b) => popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id))
+ return items
+ })
+
+ const source = (item: unknown) => (item as ProviderMeta).source
+
+ const disconnect = async (providerID: string, name: string) => {
+ await globalSDK.client.auth
+ .remove({ providerID })
+ .then(async () => {
+ await globalSDK.client.global.dispose()
+ showToast({
+ variant: "success",
+ icon: "circle-check",
+ title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }),
+ description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }),
+ })
+ })
+ .catch((err: unknown) => {
+ const message = err instanceof Error ? err.message : String(err)
+ showToast({ title: language.t("common.requestFailed"), description: message })
+ })
+ }
return (
- <div class="flex flex-col h-full overflow-y-auto">
- <div class="flex flex-col gap-6 p-6 max-w-[600px]">
- <h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
- <p class="text-14-regular text-text-weak">{language.t("settings.providers.description")}</p>
+ <div class="flex flex-col h-full overflow-y-auto no-scrollbar" style={{ padding: "0 40px 40px 40px" }}>
+ <div
+ class="sticky top-0 z-10"
+ style={{
+ background:
+ "linear-gradient(to bottom, var(--surface-raised-stronger-non-alpha) calc(100% - 24px), transparent)",
+ }}
+ >
+ <div class="flex flex-col gap-4 pt-6 pb-6 max-w-[720px]">
+ <div class="flex items-center justify-between gap-4">
+ <h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
+ </div>
+ </div>
+ </div>
+
+ <div class="flex flex-col gap-8 max-w-[720px]">
+ <div class="flex flex-col gap-1">
+ <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.connected")}</h3>
+ <div class="bg-surface-raised-base px-4 rounded-lg">
+ <Show
+ when={connected().length > 0}
+ fallback={
+ <div class="py-4 text-14-regular text-text-weak">
+ {language.t("settings.providers.connected.empty")}
+ </div>
+ }
+ >
+ <For each={connected()}>
+ {(item) => (
+ <div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
+ <div class="flex items-center gap-3 min-w-0">
+ <ProviderIcon id={item.id as IconName} class="size-5 shrink-0 icon-strong-base" />
+ <span class="text-14-regular text-text-strong truncate">{item.name}</span>
+ <Show when={source(item) === "env"}>
+ <Tag>{language.t("settings.providers.tag.environment")}</Tag>
+ </Show>
+ <Show when={source(item) === "api"}>
+ <Tag>{language.t("provider.connect.method.apiKey")}</Tag>
+ </Show>
+ </div>
+ <Show when={source(item) === "api"}>
+ <Button size="small" variant="ghost" onClick={() => void disconnect(item.id, item.name)}>
+ {language.t("common.disconnect")}
+ </Button>
+ </Show>
+ </div>
+ )}
+ </For>
+ </Show>
+ </div>
+ </div>
+
+ <div class="flex flex-col gap-1">
+ <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.popular")}</h3>
+ <div class="bg-surface-raised-base px-4 rounded-lg">
+ <For each={popular()}>
+ {(item) => (
+ <div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
+ <div class="flex items-center gap-x-3 min-w-0">
+ <ProviderIcon id={item.id as IconName} class="size-5 shrink-0 icon-strong-base" />
+ <span class="text-14-regular text-text-strong">{item.name}</span>
+ <Show when={item.id === "opencode"}>
+ <Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
+ </Show>
+ <Show when={item.id === "anthropic"}>
+ <div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
+ </Show>
+ <Show when={item.id === "openai"}>
+ <div class="text-14-regular text-text-weak">{language.t("dialog.provider.openai.note")}</div>
+ </Show>
+ <Show when={item.id.startsWith("github-copilot")}>
+ <div class="text-14-regular text-text-weak">{language.t("dialog.provider.copilot.note")}</div>
+ </Show>
+ </div>
+ <Button
+ size="small"
+ variant="secondary"
+ icon="plus-small"
+ onClick={() => {
+ dialog.show(() => <DialogConnectProvider provider={item.id} />)
+ }}
+ >
+ {language.t("common.connect")}
+ </Button>
+ </div>
+ )}
+ </For>
+ </div>
+
+ <Button
+ variant="ghost"
+ class="w-full justify-start px-[11px] py-3.5 gap-4.5 text-14-medium"
+ icon="dot-grid"
+ onClick={() => {
+ dialog.show(() => <DialogSelectProvider />)
+ }}
+ >
+ {language.t("dialog.provider.viewAll")}
+ </Button>
+ </div>
</div>
</div>
)
diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts
index b32f03485..a34c8ef21 100644
--- a/packages/app/src/i18n/en.ts
+++ b/packages/app/src/i18n/en.ts
@@ -137,6 +137,9 @@ export const dict = {
"provider.connect.toast.connected.title": "{{provider}} connected",
"provider.connect.toast.connected.description": "{{provider}} models are now available to use.",
+ "provider.disconnect.toast.disconnected.title": "{{provider}} disconnected",
+ "provider.disconnect.toast.disconnected.description": "{{provider}} models are no longer available.",
+
"model.tag.free": "Free",
"model.tag.latest": "Latest",
"model.provider.anthropic": "Anthropic",
@@ -159,6 +162,8 @@ export const dict = {
"common.loading": "Loading",
"common.loading.ellipsis": "...",
"common.cancel": "Cancel",
+ "common.connect": "Connect",
+ "common.disconnect": "Disconnect",
"common.submit": "Submit",
"common.save": "Save",
"common.saving": "Saving...",
@@ -491,6 +496,7 @@ export const dict = {
"sidebar.project.viewAllSessions": "View all sessions",
"settings.section.desktop": "Desktop",
+ "settings.section.server": "Server",
"settings.tab.general": "General",
"settings.tab.shortcuts": "Shortcuts",
@@ -599,6 +605,10 @@ export const dict = {
"settings.providers.title": "Providers",
"settings.providers.description": "Provider settings will be configurable here.",
+ "settings.providers.section.connected": "Connected providers",
+ "settings.providers.connected.empty": "No connected providers",
+ "settings.providers.section.popular": "Popular providers",
+ "settings.providers.tag.environment": "Environment",
"settings.models.title": "Models",
"settings.models.description": "Model settings will be configurable here.",
"settings.agents.title": "Agents",