summaryrefslogtreecommitdiffhomepage
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
parenta5b72a7d994618555467b4269f48b0c000e2db84 (diff)
downloadopencode-03d884797c19e7cbe92d7ef237c4b28d51b58f18.tar.gz
opencode-03d884797c19e7cbe92d7ef237c4b28d51b58f18.zip
wip(app): provider settings
-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
-rw-r--r--packages/opencode/src/server/server.ts30
-rw-r--r--packages/sdk/js/src/v2/gen/sdk.gen.ts32
-rw-r--r--packages/sdk/js/src/v2/gen/types.gen.ts29
-rw-r--r--packages/sdk/openapi.json50
7 files changed, 322 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",
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index fa646f21e..302c5376d 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -441,6 +441,36 @@ export namespace Server {
return c.json(true)
},
)
+ .delete(
+ "/auth/:providerID",
+ describeRoute({
+ summary: "Remove auth credentials",
+ description: "Remove authentication credentials",
+ operationId: "auth.remove",
+ responses: {
+ 200: {
+ description: "Successfully removed authentication credentials",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ ...errors(400),
+ },
+ }),
+ validator(
+ "param",
+ z.object({
+ providerID: z.string(),
+ }),
+ ),
+ async (c) => {
+ const providerID = c.req.valid("param").providerID
+ await Auth.remove(providerID)
+ return c.json(true)
+ },
+ )
.get(
"/event",
describeRoute({
diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts
index 67e7ac80c..d39dd2b34 100644
--- a/packages/sdk/js/src/v2/gen/sdk.gen.ts
+++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts
@@ -9,6 +9,8 @@ import type {
AppLogResponses,
AppSkillsResponses,
Auth as Auth3,
+ AuthRemoveErrors,
+ AuthRemoveResponses,
AuthSetErrors,
AuthSetResponses,
CommandListResponses,
@@ -3055,6 +3057,36 @@ export class Formatter extends HeyApiClient {
export class Auth2 extends HeyApiClient {
/**
+ * Remove auth credentials
+ *
+ * Remove authentication credentials
+ */
+ public remove<ThrowOnError extends boolean = false>(
+ parameters: {
+ providerID: string
+ directory?: string
+ },
+ options?: Options<never, ThrowOnError>,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "path", key: "providerID" },
+ { in: "query", key: "directory" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).delete<AuthRemoveResponses, AuthRemoveErrors, ThrowOnError>({
+ url: "/auth/{providerID}",
+ ...options,
+ ...params,
+ })
+ }
+
+ /**
* Set auth credentials
*
* Set authentication credentials
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index 38a52b325..9258bc0cd 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -4867,6 +4867,35 @@ export type FormatterStatusResponses = {
export type FormatterStatusResponse = FormatterStatusResponses[keyof FormatterStatusResponses]
+export type AuthRemoveData = {
+ body?: never
+ path: {
+ providerID: string
+ }
+ query?: {
+ directory?: string
+ }
+ url: "/auth/{providerID}"
+}
+
+export type AuthRemoveErrors = {
+ /**
+ * Bad request
+ */
+ 400: BadRequestError
+}
+
+export type AuthRemoveError = AuthRemoveErrors[keyof AuthRemoveErrors]
+
+export type AuthRemoveResponses = {
+ /**
+ * Successfully removed authentication credentials
+ */
+ 200: boolean
+}
+
+export type AuthRemoveResponse = AuthRemoveResponses[keyof AuthRemoveResponses]
+
export type AuthSetData = {
body?: Auth
path: {
diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json
index a029d0ef0..8808bcf7d 100644
--- a/packages/sdk/openapi.json
+++ b/packages/sdk/openapi.json
@@ -5709,6 +5709,56 @@
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.set({\n ...\n})"
}
]
+ },
+ "delete": {
+ "operationId": "auth.remove",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "providerID",
+ "schema": {
+ "type": "string"
+ },
+ "required": true
+ }
+ ],
+ "summary": "Remove auth credentials",
+ "description": "Remove authentication credentials",
+ "responses": {
+ "200": {
+ "description": "Successfully removed authentication credentials",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.remove({\n ...\n})"
+ }
+ ]
}
},
"/event": {