diff options
| author | adamelmore <[email protected]> | 2026-01-25 15:59:56 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-01-25 20:40:00 -0600 |
| commit | 03d884797c19e7cbe92d7ef237c4b28d51b58f18 (patch) | |
| tree | 3bea0b88823ce6ef0a6f83cff90c3a14b9432610 | |
| parent | a5b72a7d994618555467b4269f48b0c000e2db84 (diff) | |
| download | opencode-03d884797c19e7cbe92d7ef237c4b28d51b58f18.tar.gz opencode-03d884797c19e7cbe92d7ef237c4b28d51b58f18.zip | |
wip(app): provider settings
| -rw-r--r-- | packages/app/src/components/dialog-settings.tsx | 68 | ||||
| -rw-r--r-- | packages/app/src/components/settings-providers.tsx | 149 | ||||
| -rw-r--r-- | packages/app/src/i18n/en.ts | 10 | ||||
| -rw-r--r-- | packages/opencode/src/server/server.ts | 30 | ||||
| -rw-r--r-- | packages/sdk/js/src/v2/gen/sdk.gen.ts | 32 | ||||
| -rw-r--r-- | packages/sdk/js/src/v2/gen/types.gen.ts | 29 | ||||
| -rw-r--r-- | packages/sdk/openapi.json | 50 |
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": { |
