mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-07-10 00:14:27 +00:00
feat(ui): add instance-scoped provider manager from model selector (#476)
## Summary - move provider authentication management out of global settings and into the model selector so it always runs against a live OpenCode instance - add an instance-scoped provider manager modal with API-key and OAuth flows, cancellable OAuth waiting, and provider discovery/configuration details - handle configured provider sources differently so config- and env-backed providers are explained in the UI while auth-backed providers can be disconnected safely ## Validation - npm run typecheck --workspace @codenomad/ui - npm run build --workspace @codenomad/ui
This commit is contained in:
parent
02c26226ab
commit
4ddd25602e
14 changed files with 1548 additions and 6 deletions
|
|
@ -1,11 +1,12 @@
|
|||
import { Combobox } from "@kobalte/core/combobox"
|
||||
import { createEffect, createMemo, createSignal } from "solid-js"
|
||||
import { providers, fetchProviders } from "../stores/sessions"
|
||||
import { ChevronDown, Star } from "lucide-solid"
|
||||
import { ChevronDown, PlugZap, Star } from "lucide-solid"
|
||||
import type { Model } from "../types/session"
|
||||
import { useI18n } from "../lib/i18n"
|
||||
import { getLogger } from "../lib/logger"
|
||||
import { uiState, toggleFavoriteModelPreference } from "../stores/preferences"
|
||||
import { ProviderManagerModal } from "./provider-auth/provider-manager-modal"
|
||||
const log = getLogger("session")
|
||||
|
||||
interface ModelSelectorProps {
|
||||
|
|
@ -52,6 +53,7 @@ export default function ModelSelector(props: ModelSelectorProps) {
|
|||
const [initialQuery, setInitialQuery] = createSignal("")
|
||||
const [initialQueryReady, setInitialQueryReady] = createSignal(false)
|
||||
const [inputValue, setInputValue] = createSignal("")
|
||||
const [providersModalOpen, setProvidersModalOpen] = createSignal(false)
|
||||
let triggerRef!: HTMLButtonElement
|
||||
let searchInputRef!: HTMLInputElement
|
||||
let listboxRef!: HTMLUListElement
|
||||
|
|
@ -404,6 +406,27 @@ export default function ModelSelector(props: ModelSelectorProps) {
|
|||
</div>
|
||||
<Combobox.Listbox ref={listboxRef} class="selector-listbox" />
|
||||
<div class="selector-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="selector-option selector-option-action w-full"
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setIsOpen(false)
|
||||
setProvidersModalOpen(true)
|
||||
}}
|
||||
>
|
||||
<PlugZap class="w-4 h-4" />
|
||||
<span class="selector-option-label">{t("modelSelector.manageProviders")}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="selector-option selector-option-action w-full"
|
||||
|
|
@ -428,6 +451,7 @@ export default function ModelSelector(props: ModelSelectorProps) {
|
|||
</Combobox.Content>
|
||||
</Combobox.Portal>
|
||||
</Combobox>
|
||||
<ProviderManagerModal instanceId={props.instanceId} open={providersModalOpen()} onOpenChange={setProvidersModalOpen} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,650 @@
|
|||
import { Dialog } from "@kobalte/core/dialog"
|
||||
import { Select } from "@kobalte/core/select"
|
||||
import { createEffect, createMemo, createSignal, For, Show, type Component } from "solid-js"
|
||||
import { Check, ChevronDown, ExternalLink, KeyRound, Loader2, PlugZap, RefreshCw, ShieldCheck, X } from "lucide-solid"
|
||||
import type { OpencodeClient } from "@opencode-ai/sdk/v2/client"
|
||||
import { openExternalUrl } from "../../lib/external-url"
|
||||
import { useI18n } from "../../lib/i18n"
|
||||
import { requestData } from "../../lib/opencode-api"
|
||||
import { isTauriHost } from "../../lib/runtime-env"
|
||||
import {
|
||||
extractProviderAuthErrorMessage,
|
||||
genericApiMethod,
|
||||
isAbortError,
|
||||
shouldShowProviderAuthPrompt,
|
||||
type ProviderAuthAuthorization,
|
||||
type ProviderAuthMethod,
|
||||
} from "../../lib/provider-auth"
|
||||
import { instances } from "../../stores/instances"
|
||||
import { fetchProviders } from "../../stores/sessions"
|
||||
|
||||
type AuthStage = "idle" | "prompts" | "authorizing" | "code" | "waiting" | "success" | "error"
|
||||
|
||||
type MethodOption = {
|
||||
value: string
|
||||
label: string
|
||||
method: ProviderAuthMethod
|
||||
index: number
|
||||
}
|
||||
|
||||
type ConfigurableProviderOption = {
|
||||
id: string
|
||||
name: string
|
||||
modelCount: number
|
||||
connectionSummary: string
|
||||
}
|
||||
|
||||
type ListedProvider = {
|
||||
id: string
|
||||
name: string
|
||||
modelCount: number
|
||||
source: "env" | "config" | "custom" | "api" | "unknown"
|
||||
}
|
||||
|
||||
type DisconnectMode = "auth-remove" | "disable-in-config" | "not-disconnectable" | "unknown"
|
||||
|
||||
interface ProviderManagerModalProps {
|
||||
instanceId: string
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
function modelCountFromProvider(provider: any): number {
|
||||
const models = provider?.models
|
||||
if (Array.isArray(models)) return models.length
|
||||
if (models && typeof models === "object") return Object.keys(models).length
|
||||
return 0
|
||||
}
|
||||
|
||||
export const ProviderManagerModal: Component<ProviderManagerModalProps> = (props) => {
|
||||
const { t } = useI18n()
|
||||
const [methodsByProvider, setMethodsByProvider] = createSignal<Record<string, ProviderAuthMethod[]>>({})
|
||||
const [availableProviders, setAvailableProviders] = createSignal<ListedProvider[]>([])
|
||||
const [connectedProviderIds, setConnectedProviderIds] = createSignal<Set<string>>(new Set())
|
||||
const [configuredProviderIds, setConfiguredProviderIds] = createSignal<Set<string>>(new Set())
|
||||
const [configData, setConfigData] = createSignal<Record<string, any>>({})
|
||||
const [selectedProviderId, setSelectedProviderId] = createSignal<string | null>(null)
|
||||
const [activeProviderId, setActiveProviderId] = createSignal<string | null>(null)
|
||||
const [selectedMethodIndex, setSelectedMethodIndex] = createSignal(0)
|
||||
const [apiKey, setApiKey] = createSignal("")
|
||||
const [promptValues, setPromptValues] = createSignal<Record<string, string>>({})
|
||||
const [authorization, setAuthorization] = createSignal<ProviderAuthAuthorization | null>(null)
|
||||
const [code, setCode] = createSignal("")
|
||||
const [stage, setStage] = createSignal<AuthStage>("idle")
|
||||
const [loading, setLoading] = createSignal(false)
|
||||
const [loadError, setLoadError] = createSignal<string | null>(null)
|
||||
const [actionError, setActionError] = createSignal<string | null>(null)
|
||||
const [authorizationLaunchBlocked, setAuthorizationLaunchBlocked] = createSignal(false)
|
||||
const [authorizationLinkCopied, setAuthorizationLinkCopied] = createSignal(false)
|
||||
let callbackAbortController: AbortController | null = null
|
||||
let pendingOauthPopup: Window | null = null
|
||||
|
||||
const instance = createMemo(() => instances().get(props.instanceId) ?? null)
|
||||
const client = createMemo<OpencodeClient | null>(() => instance()?.client ?? null)
|
||||
|
||||
const providerNameById = createMemo(() => {
|
||||
const names = new Map<string, string>()
|
||||
for (const provider of availableProviders()) {
|
||||
names.set(provider.id, provider.name || provider.id)
|
||||
}
|
||||
return names
|
||||
})
|
||||
|
||||
const configurableProviders = createMemo<ConfigurableProviderOption[]>(() => {
|
||||
const ids = new Set<string>()
|
||||
for (const provider of availableProviders()) ids.add(provider.id)
|
||||
for (const id of Object.keys(methodsByProvider())) ids.add(id)
|
||||
return Array.from(ids)
|
||||
.sort((left, right) => left.localeCompare(right, undefined, { sensitivity: "base" }))
|
||||
.map((id) => {
|
||||
const listed = availableProviders().find((provider) => provider.id === id)
|
||||
return {
|
||||
id,
|
||||
name: providerNameById().get(id) ?? id,
|
||||
modelCount: listed?.modelCount ?? 0,
|
||||
connectionSummary: methodSummary(id),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const configuredProviders = createMemo(() =>
|
||||
availableProviders().filter((provider) => isConfiguredProvider(provider)),
|
||||
)
|
||||
|
||||
const getDisconnectMode = (provider: ListedProvider): DisconnectMode => {
|
||||
if (provider.source === "env") return "not-disconnectable"
|
||||
if (provider.source === "config" || configuredProviderIds().has(provider.id)) return "disable-in-config"
|
||||
if (provider.source === "api" || provider.source === "custom") return "auth-remove"
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
const isConfiguredProvider = (provider: ListedProvider) =>
|
||||
connectedProviderIds().has(provider.id) ||
|
||||
provider.source === "env" ||
|
||||
provider.source === "config" ||
|
||||
provider.source === "api" ||
|
||||
configuredProviderIds().has(provider.id)
|
||||
|
||||
const describeProviderSource = (provider: ListedProvider) => {
|
||||
const mode = getDisconnectMode(provider)
|
||||
if (mode === "disable-in-config") return t("settings.providers.source.config")
|
||||
if (mode === "not-disconnectable") return t("settings.providers.source.env")
|
||||
if (provider.source === "api") return t("settings.providers.source.api")
|
||||
if (provider.source === "custom") return t("settings.providers.source.custom")
|
||||
return t("settings.providers.source.unknown")
|
||||
}
|
||||
|
||||
const selectedProviderOption = createMemo(() =>
|
||||
configurableProviders().find((provider) => provider.id === selectedProviderId()) ?? configurableProviders()[0] ?? null,
|
||||
)
|
||||
|
||||
const activeProviderName = createMemo(() => {
|
||||
const providerId = activeProviderId()
|
||||
return providerId ? providerNameById().get(providerId) ?? providerId : ""
|
||||
})
|
||||
|
||||
const activeMethods = createMemo(() => {
|
||||
const providerId = activeProviderId()
|
||||
if (!providerId) return [genericApiMethod]
|
||||
const methods = methodsByProvider()[providerId]
|
||||
return methods && methods.length > 0 ? methods : [genericApiMethod]
|
||||
})
|
||||
|
||||
const methodOptions = createMemo<MethodOption[]>(() =>
|
||||
activeMethods().map((method, index) => ({
|
||||
value: String(index),
|
||||
label: method.label || (method.type === "oauth" ? t("settings.providers.method.oauth") : t("settings.providers.method.api")),
|
||||
method,
|
||||
index,
|
||||
})),
|
||||
)
|
||||
|
||||
const selectedMethodOption = createMemo(() => methodOptions().find((option) => option.index === selectedMethodIndex()) ?? methodOptions()[0])
|
||||
const selectedMethod = createMemo(() => selectedMethodOption()?.method ?? genericApiMethod)
|
||||
const oauthPromptsUnsupported = createMemo(() => selectedMethod().type === "oauth" && (selectedMethod().prompts?.length ?? 0) > 0)
|
||||
const visiblePrompts = createMemo(() =>
|
||||
(selectedMethod().prompts ?? []).filter((prompt) => shouldShowProviderAuthPrompt(prompt, promptValues())),
|
||||
)
|
||||
const canSubmit = createMemo(() => {
|
||||
if (!activeProviderId()) return false
|
||||
if (stage() === "authorizing" || stage() === "waiting" || stage() === "success") return false
|
||||
if (oauthPromptsUnsupported()) return false
|
||||
if (selectedMethod().type === "api") return apiKey().trim().length > 0
|
||||
return visiblePrompts().every((prompt) => (promptValues()[prompt.key] ?? "").trim().length > 0)
|
||||
})
|
||||
|
||||
function handleModalOpenChange(open: boolean) {
|
||||
if (!open) resetFlow(null)
|
||||
props.onOpenChange(open)
|
||||
}
|
||||
|
||||
function isBrowserHostForOAuth(): boolean {
|
||||
return !isTauriHost() && typeof window !== "undefined"
|
||||
}
|
||||
|
||||
function prepareOAuthPopupWindow(): Window | null {
|
||||
if (!isBrowserHostForOAuth()) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const popup = window.open("", "_blank")
|
||||
if (popup && popup.document) {
|
||||
popup.document.title = t("settings.providers.oauth.popup.loadingTitle")
|
||||
popup.document.body.innerHTML = `<div style=\"font-family: sans-serif; padding: 24px; color: #111;\">${t("settings.providers.oauth.popup.loadingBody")}</div>`
|
||||
}
|
||||
return popup
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function launchAuthorizationUrl(url: string, options?: { popup?: Window | null; sameTab?: boolean }): Promise<boolean> {
|
||||
if (options?.sameTab && typeof window !== "undefined") {
|
||||
window.location.assign(url)
|
||||
return true
|
||||
}
|
||||
|
||||
const popup = options?.popup
|
||||
if (popup && !popup.closed) {
|
||||
try {
|
||||
popup.location.href = url
|
||||
return true
|
||||
} catch {
|
||||
// fall through to general opener path
|
||||
}
|
||||
}
|
||||
|
||||
return await openExternalUrl(url, "provider-auth")
|
||||
}
|
||||
|
||||
async function copyAuthorizationUrl(): Promise<void> {
|
||||
const url = authorization()?.url
|
||||
if (!url || typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(url)
|
||||
setAuthorizationLinkCopied(true)
|
||||
setTimeout(() => setAuthorizationLinkCopied(false), 1500)
|
||||
} catch {
|
||||
setAuthorizationLinkCopied(false)
|
||||
}
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!props.open) return
|
||||
const authClient = client()
|
||||
if (!authClient) return
|
||||
void loadProviderData(authClient)
|
||||
})
|
||||
|
||||
async function loadProviderData(authClient: OpencodeClient): Promise<void> {
|
||||
setLoading(true)
|
||||
setLoadError(null)
|
||||
try {
|
||||
const [providerListResponse, authResponse, configResponse] = await Promise.all([
|
||||
(authClient as any).provider.list(),
|
||||
(authClient as any).provider.auth(),
|
||||
(authClient as any).config.get(),
|
||||
])
|
||||
const nextConfigData = (configResponse?.data ?? {}) as Record<string, any>
|
||||
const nextConfiguredIds = new Set(Object.keys((nextConfigData.provider ?? {}) as Record<string, unknown>))
|
||||
const listed = ((providerListResponse?.data?.all ?? []) as any[]).map((provider) => ({
|
||||
id: String(provider.id ?? ""),
|
||||
name: String(provider.name ?? provider.id ?? ""),
|
||||
modelCount: modelCountFromProvider(provider),
|
||||
source:
|
||||
provider?.source === "env" || provider?.source === "config" || provider?.source === "custom" || provider?.source === "api"
|
||||
? provider.source
|
||||
: "unknown",
|
||||
})).filter((provider) => provider.id.length > 0)
|
||||
setAvailableProviders(listed)
|
||||
setConnectedProviderIds(new Set((providerListResponse?.data?.connected ?? []) as string[]))
|
||||
setConfiguredProviderIds(nextConfiguredIds)
|
||||
setConfigData(nextConfigData)
|
||||
setMethodsByProvider((authResponse?.data ?? {}) as Record<string, ProviderAuthMethod[]>)
|
||||
setSelectedProviderId((current) => current ?? listed[0]?.id ?? Object.keys(authResponse?.data ?? {})[0] ?? null)
|
||||
} catch (error) {
|
||||
setLoadError(extractProviderAuthErrorMessage(error, t("settings.providers.errors.loadFailed")))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
function resetFlow(nextProviderId: string | null = null) {
|
||||
callbackAbortController?.abort()
|
||||
callbackAbortController = null
|
||||
if (pendingOauthPopup && !pendingOauthPopup.closed) {
|
||||
pendingOauthPopup.close()
|
||||
}
|
||||
pendingOauthPopup = null
|
||||
setActiveProviderId(nextProviderId)
|
||||
setSelectedMethodIndex(0)
|
||||
setApiKey("")
|
||||
setPromptValues({})
|
||||
setAuthorization(null)
|
||||
setCode("")
|
||||
setStage(nextProviderId ? "prompts" : "idle")
|
||||
setActionError(null)
|
||||
setAuthorizationLaunchBlocked(false)
|
||||
setAuthorizationLinkCopied(false)
|
||||
}
|
||||
|
||||
function updatePromptValue(key: string, value: string) {
|
||||
setPromptValues((current) => ({ ...current, [key]: value }))
|
||||
}
|
||||
|
||||
async function refreshAfterAuth(authClient: OpencodeClient) {
|
||||
await (authClient as any).global.dispose().catch(() => undefined)
|
||||
await fetchProviders(props.instanceId).catch(() => undefined)
|
||||
await loadProviderData(authClient).catch(() => undefined)
|
||||
}
|
||||
|
||||
async function submitApiAuth(providerId: string, authClient: OpencodeClient) {
|
||||
await requestData(
|
||||
(authClient as any).auth.set({ providerID: providerId, auth: { type: "api", key: apiKey().trim() } }),
|
||||
"auth.set",
|
||||
)
|
||||
await refreshAfterAuth(authClient)
|
||||
resetFlow(null)
|
||||
}
|
||||
|
||||
async function submitOAuthAuthorize(providerId: string, authClient: OpencodeClient) {
|
||||
const response = await (authClient as any).provider.oauth.authorize(
|
||||
{ providerID: providerId, method: selectedMethodIndex() },
|
||||
{ throwOnError: true },
|
||||
)
|
||||
const data = response?.data as ProviderAuthAuthorization | undefined
|
||||
if (!data) throw new Error(t("settings.providers.errors.noAuthorization"))
|
||||
setAuthorization(data)
|
||||
const opened = await launchAuthorizationUrl(data.url, { popup: pendingOauthPopup })
|
||||
pendingOauthPopup = null
|
||||
setAuthorizationLaunchBlocked(!opened)
|
||||
if (data.method === "code") {
|
||||
setStage("code")
|
||||
return
|
||||
}
|
||||
setStage("waiting")
|
||||
callbackAbortController = new AbortController()
|
||||
await requestData(
|
||||
(authClient as any).provider.oauth.callback(
|
||||
{ providerID: providerId, method: selectedMethodIndex() },
|
||||
{ signal: callbackAbortController.signal },
|
||||
),
|
||||
"provider.oauth.callback",
|
||||
)
|
||||
callbackAbortController = null
|
||||
await refreshAfterAuth(authClient)
|
||||
resetFlow(null)
|
||||
}
|
||||
|
||||
async function submitAuth() {
|
||||
const providerId = activeProviderId()
|
||||
const authClient = client()
|
||||
if (!providerId || !authClient || !canSubmit()) return
|
||||
setStage("authorizing")
|
||||
setActionError(null)
|
||||
try {
|
||||
if (selectedMethod().type === "api") {
|
||||
await submitApiAuth(providerId, authClient)
|
||||
return
|
||||
}
|
||||
pendingOauthPopup = prepareOAuthPopupWindow()
|
||||
setAuthorizationLaunchBlocked(isBrowserHostForOAuth() && pendingOauthPopup === null)
|
||||
await submitOAuthAuthorize(providerId, authClient)
|
||||
} catch (error) {
|
||||
if (pendingOauthPopup && !pendingOauthPopup.closed) {
|
||||
pendingOauthPopup.close()
|
||||
}
|
||||
pendingOauthPopup = null
|
||||
if (isAbortError(error)) {
|
||||
setStage("prompts")
|
||||
return
|
||||
}
|
||||
setActionError(extractProviderAuthErrorMessage(error, t("settings.providers.errors.authorizationFailed")))
|
||||
setStage("error")
|
||||
}
|
||||
}
|
||||
|
||||
async function submitOAuthCode() {
|
||||
const providerId = activeProviderId()
|
||||
const authClient = client()
|
||||
if (!providerId || !authClient || !code().trim()) return
|
||||
setStage("authorizing")
|
||||
setActionError(null)
|
||||
try {
|
||||
await requestData(
|
||||
(authClient as any).provider.oauth.callback({ providerID: providerId, method: selectedMethodIndex(), code: code().trim() }),
|
||||
"provider.oauth.callback",
|
||||
)
|
||||
await refreshAfterAuth(authClient)
|
||||
resetFlow(null)
|
||||
} catch (error) {
|
||||
setActionError(extractProviderAuthErrorMessage(error, t("settings.providers.errors.authorizationFailed")))
|
||||
setStage("code")
|
||||
}
|
||||
}
|
||||
|
||||
async function disconnectProvider(providerId: string) {
|
||||
const authClient = client()
|
||||
const provider = availableProviders().find((item) => item.id === providerId)
|
||||
if (!authClient || !provider) return
|
||||
setActionError(null)
|
||||
setStage("authorizing")
|
||||
try {
|
||||
const disconnectMode = getDisconnectMode(provider)
|
||||
if (disconnectMode === "not-disconnectable") {
|
||||
setActionError(t("settings.providers.errors.envDisconnectUnavailable"))
|
||||
setStage("error")
|
||||
return
|
||||
}
|
||||
|
||||
if (disconnectMode === "disable-in-config") {
|
||||
const disabledProviders = Array.isArray(configData().disabled_providers)
|
||||
? [...configData().disabled_providers]
|
||||
: []
|
||||
if (!disabledProviders.includes(providerId)) {
|
||||
disabledProviders.push(providerId)
|
||||
}
|
||||
await requestData(
|
||||
(authClient as any).config.update({
|
||||
config: {
|
||||
...configData(),
|
||||
disabled_providers: disabledProviders,
|
||||
},
|
||||
}),
|
||||
"config.update",
|
||||
)
|
||||
} else {
|
||||
await requestData((authClient as any).auth.remove({ providerID: providerId }), "auth.remove")
|
||||
}
|
||||
await refreshAfterAuth(authClient)
|
||||
resetFlow(null)
|
||||
} catch (error) {
|
||||
setActionError(extractProviderAuthErrorMessage(error, t("settings.providers.errors.removeFailed")))
|
||||
setStage("error")
|
||||
}
|
||||
}
|
||||
|
||||
function cancelOAuthWait() {
|
||||
callbackAbortController?.abort()
|
||||
callbackAbortController = null
|
||||
setStage("prompts")
|
||||
setAuthorization(null)
|
||||
setActionError(null)
|
||||
}
|
||||
|
||||
function methodSummary(providerId: string) {
|
||||
const methods = methodsByProvider()[providerId]
|
||||
if (!methods || methods.length === 0) return t("settings.providers.method.fallback")
|
||||
const kinds = new Set(methods.map((method) => method.type))
|
||||
if (kinds.size > 1) return t("settings.providers.method.mixed")
|
||||
if (kinds.has("oauth")) return t("settings.providers.method.oauth")
|
||||
return t("settings.providers.method.api")
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={props.open} onOpenChange={handleModalOpenChange}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class="modal-overlay" />
|
||||
<Dialog.Content class="modal-surface providers-manager-modal">
|
||||
<div class="providers-manager-header">
|
||||
<div class="settings-card-heading-with-icon">
|
||||
<PlugZap class="settings-card-heading-icon" />
|
||||
<div>
|
||||
<Dialog.Title class="providers-manager-title">{t("settings.providers.title")}</Dialog.Title>
|
||||
<p class="settings-card-subtitle">{t("settings.providers.subtitle")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="selector-button selector-button-secondary settings-screen-close" onClick={() => handleModalOpenChange(false)} aria-label={t("settings.close")}>
|
||||
<X class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="providers-manager-body">
|
||||
<Show when={!client()}>
|
||||
<div class="settings-card-message">{t("settings.providers.empty.noInstance")}</div>
|
||||
</Show>
|
||||
|
||||
<Show when={client()}>
|
||||
<div class="providers-connect-bar">
|
||||
<Select<ConfigurableProviderOption>
|
||||
value={selectedProviderOption()}
|
||||
onChange={(option) => option && setSelectedProviderId(option.id)}
|
||||
options={configurableProviders()}
|
||||
optionValue="id"
|
||||
optionTextValue="name"
|
||||
itemComponent={(itemProps) => (
|
||||
<Select.Item item={itemProps.item} class="selector-option selector-option--multiline">
|
||||
<div class="selector-option-content">
|
||||
<Select.ItemLabel class="selector-option-label">{itemProps.item.rawValue.name}</Select.ItemLabel>
|
||||
<div class="selector-option-description">
|
||||
<span dir="ltr">{itemProps.item.rawValue.id}</span>
|
||||
<span> • </span>
|
||||
<span>
|
||||
{itemProps.item.rawValue.modelCount === 1
|
||||
? t("settings.providers.models.one", { count: itemProps.item.rawValue.modelCount })
|
||||
: t("settings.providers.models.other", { count: itemProps.item.rawValue.modelCount })}
|
||||
</span>
|
||||
<span> • </span>
|
||||
<span>{itemProps.item.rawValue.connectionSummary}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Select.Item>
|
||||
)}
|
||||
>
|
||||
<Select.Trigger class="selector-trigger providers-connect-select" aria-label={t("settings.providers.selectProvider") }>
|
||||
<div class="flex-1 min-w-0">
|
||||
<Select.Value<ConfigurableProviderOption>>
|
||||
{(state) => (
|
||||
<div class="selector-trigger-label selector-trigger-label--stacked flex-1 min-w-0">
|
||||
<span class="selector-trigger-primary selector-trigger-primary--align-left">{state.selectedOption()?.name ?? t("settings.providers.selectProvider")}</span>
|
||||
<Show when={state.selectedOption()}>
|
||||
<span class="selector-trigger-secondary" dir="ltr">
|
||||
{state.selectedOption()?.id} • {state.selectedOption()?.connectionSummary}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</Select.Value>
|
||||
</div>
|
||||
<Select.Icon class="selector-trigger-icon"><ChevronDown class="w-3 h-3" /></Select.Icon>
|
||||
</Select.Trigger>
|
||||
<Select.Portal><Select.Content class="selector-popover"><Select.Listbox class="selector-listbox" /></Select.Content></Select.Portal>
|
||||
</Select>
|
||||
<button type="button" class="selector-button selector-button-primary" disabled={!selectedProviderOption()} onClick={() => resetFlow(selectedProviderOption()?.id ?? null)}>
|
||||
{t("settings.providers.actions.connect")}
|
||||
</button>
|
||||
<button type="button" class="settings-pill-button" disabled={loading()} onClick={() => client() && void loadProviderData(client()!)}>
|
||||
<RefreshCw class={loading() ? "providers-spin-icon" : "providers-button-icon"} />
|
||||
{t("settings.providers.refresh")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Show when={loadError()}>
|
||||
<div class="settings-error-message">{loadError()}</div>
|
||||
</Show>
|
||||
|
||||
<Show when={activeProviderId()}>
|
||||
<section class="providers-connect-panel">
|
||||
<div class="providers-panel-header">
|
||||
<div>
|
||||
<h3 class="settings-card-title">{t("settings.providers.auth.title", { provider: activeProviderName() })}</h3>
|
||||
<p class="settings-card-subtitle">{t("settings.providers.auth.subtitle")}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="selector-button selector-button-secondary settings-screen-close"
|
||||
onClick={() => resetFlow(null)}
|
||||
aria-label={t("settings.providers.actions.close")}
|
||||
title={t("settings.providers.actions.close")}
|
||||
>
|
||||
<X class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Show when={methodOptions().length > 1}>
|
||||
<div class="settings-toggle-row settings-toggle-row-compact providers-method-row">
|
||||
<div><div class="settings-toggle-title">{t("settings.providers.method.title")}</div><div class="settings-toggle-caption">{t("settings.providers.method.subtitle")}</div></div>
|
||||
<Select<MethodOption>
|
||||
value={selectedMethodOption()}
|
||||
onChange={(option) => {
|
||||
if (!option) return
|
||||
setSelectedMethodIndex(option.index)
|
||||
setPromptValues({})
|
||||
setApiKey("")
|
||||
setAuthorization(null)
|
||||
setCode("")
|
||||
setStage("prompts")
|
||||
setActionError(null)
|
||||
}}
|
||||
options={methodOptions()}
|
||||
optionValue="value"
|
||||
optionTextValue="label"
|
||||
itemComponent={(itemProps) => <Select.Item item={itemProps.item} class="selector-option"><Select.ItemLabel class="selector-option-label">{itemProps.item.rawValue.label}</Select.ItemLabel></Select.Item>}
|
||||
>
|
||||
<Select.Trigger class="selector-trigger providers-method-trigger" aria-label={t("settings.providers.method.title")}>
|
||||
<div class="flex-1 min-w-0"><Select.Value<MethodOption>>{(state) => <span class="selector-trigger-primary selector-trigger-primary--align-left">{state.selectedOption()?.label}</span>}</Select.Value></div>
|
||||
<Select.Icon class="selector-trigger-icon"><ChevronDown class="w-3 h-3" /></Select.Icon>
|
||||
</Select.Trigger>
|
||||
<Select.Portal><Select.Content class="selector-popover"><Select.Listbox class="selector-listbox" /></Select.Content></Select.Portal>
|
||||
</Select>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={selectedMethod().type === "api"}>
|
||||
<div class="providers-form-stack"><label class="providers-field"><span class="settings-form-label">{t("settings.providers.apiKey.label")}</span><div class="providers-input-wrap"><KeyRound class="providers-input-icon" /><input type="password" class="providers-input" value={apiKey()} onInput={(event) => setApiKey(event.currentTarget.value)} placeholder={t("settings.providers.apiKey.placeholder")} autocomplete="off" /></div></label></div>
|
||||
</Show>
|
||||
|
||||
<Show when={selectedMethod().type === "oauth" && (stage() === "prompts" || stage() === "error" || stage() === "authorizing")}>
|
||||
<div class="providers-form-stack">
|
||||
<Show when={oauthPromptsUnsupported()}>
|
||||
<div class="settings-error-message">{t("settings.providers.oauth.promptsUnsupported")}</div>
|
||||
</Show>
|
||||
<Show when={visiblePrompts().length === 0}><div class="settings-card-message">{t("settings.providers.oauth.noPrompts")}</div></Show>
|
||||
<For each={oauthPromptsUnsupported() ? [] : visiblePrompts()}>{(prompt) => (
|
||||
<div class="providers-field">
|
||||
<label class="settings-form-label" for={`provider-prompt-${prompt.key}`}>{prompt.message}</label>
|
||||
<Show when={prompt.type === "select"} fallback={<input id={`provider-prompt-${prompt.key}`} type="text" class="providers-input" value={promptValues()[prompt.key] ?? ""} onInput={(event) => updatePromptValue(prompt.key, event.currentTarget.value)} placeholder={prompt.type === "text" ? prompt.placeholder : undefined} />}>
|
||||
<Select<{ label: string; value: string; hint?: string }> value={(prompt.type === "select" ? prompt.options : []).find((option) => option.value === promptValues()[prompt.key])} onChange={(option) => option && updatePromptValue(prompt.key, option.value)} options={prompt.type === "select" ? prompt.options : []} optionValue="value" optionTextValue="label" itemComponent={(itemProps) => <Select.Item item={itemProps.item} class="selector-option"><Select.ItemLabel class="selector-option-label">{itemProps.item.rawValue.label}<Show when={itemProps.item.rawValue.hint}><span class="providers-select-hint">{itemProps.item.rawValue.hint}</span></Show></Select.ItemLabel></Select.Item>}>
|
||||
<Select.Trigger class="selector-trigger providers-prompt-trigger" aria-label={prompt.message}><div class="flex-1 min-w-0"><Select.Value<{ label: string; value: string; hint?: string }>>{(state) => <span class="selector-trigger-primary selector-trigger-primary--align-left">{state.selectedOption()?.label ?? t("settings.providers.prompt.selectPlaceholder")}</span>}</Select.Value></div><Select.Icon class="selector-trigger-icon"><ChevronDown class="w-3 h-3" /></Select.Icon></Select.Trigger>
|
||||
<Select.Portal><Select.Content class="selector-popover"><Select.Listbox class="selector-listbox" /></Select.Content></Select.Portal>
|
||||
</Select>
|
||||
</Show>
|
||||
</div>
|
||||
)}</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={stage() === "code"}><div class="providers-form-stack"><div class="providers-oauth-instructions"><ExternalLink class="providers-instructions-icon" /><span>{authorization()?.instructions || t("settings.providers.oauth.enterCode")}</span></div><label class="providers-field"><span class="settings-form-label">{t("settings.providers.oauth.codeLabel")}</span><input type="text" class="providers-input" value={code()} onInput={(event) => setCode(event.currentTarget.value)} placeholder={t("settings.providers.oauth.codePlaceholder")} autocomplete="one-time-code" /></label></div></Show>
|
||||
<Show when={stage() === "waiting"}><div class="providers-waiting-card"><Loader2 class="providers-spin-icon" /><div><div class="settings-toggle-title">{t("settings.providers.oauth.waitingTitle")}</div><div class="settings-toggle-caption">{authorization()?.instructions}</div></div><button type="button" class="selector-button selector-button-secondary providers-wait-cancel" onClick={cancelOAuthWait}>{t("settings.providers.oauth.cancelWait")}</button></div></Show>
|
||||
<Show when={authorization() && (stage() === "code" || stage() === "waiting")}>
|
||||
<div class="providers-oauth-actions">
|
||||
<a href={authorization()?.url} target="_blank" rel="noopener noreferrer" class="selector-button selector-button-secondary providers-oauth-link">
|
||||
<ExternalLink class="w-4 h-4" />
|
||||
{t("settings.providers.oauth.openPage")}
|
||||
</a>
|
||||
<button type="button" class="selector-button selector-button-secondary" onClick={() => void launchAuthorizationUrl(authorization()!.url, { sameTab: true })}>
|
||||
{t("settings.providers.oauth.openHere")}
|
||||
</button>
|
||||
<button type="button" class="selector-button selector-button-secondary" onClick={() => void copyAuthorizationUrl()}>
|
||||
{authorizationLinkCopied() ? t("settings.providers.oauth.linkCopied") : t("settings.providers.oauth.copyLink")}
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={authorizationLaunchBlocked() && authorization()}>
|
||||
<div class="settings-card-message">{t("settings.providers.oauth.popupBlocked")}</div>
|
||||
</Show>
|
||||
<Show when={stage() === "success"}><div class="providers-success-card"><Check class="providers-success-icon" /><span>{t("settings.providers.success")}</span></div></Show>
|
||||
<Show when={actionError()}><div class="settings-error-message">{actionError()}</div></Show>
|
||||
|
||||
<div class="providers-actions-row">
|
||||
<Show when={stage() === "code"} fallback={<button type="button" class="selector-button selector-button-primary" disabled={!canSubmit()} onClick={() => void submitAuth()}><Show when={stage() === "authorizing"} fallback={t("settings.providers.actions.continue")}><Loader2 class="providers-spin-icon" />{t("settings.providers.actions.working")}</Show></button>}>
|
||||
<button type="button" class="selector-button selector-button-primary" disabled={!code().trim()} onClick={() => void submitOAuthCode()}>{t("settings.providers.oauth.submitCode")}</button>
|
||||
</Show>
|
||||
</div>
|
||||
</section>
|
||||
</Show>
|
||||
|
||||
<section class="providers-list-section">
|
||||
<h3 class="settings-card-title">{t("settings.providers.configured.title")}</h3>
|
||||
<Show when={loading()}><div class="providers-loading-row"><Loader2 class="providers-spin-icon" /><span>{t("settings.providers.loading")}</span></div></Show>
|
||||
<Show when={!loading() && configuredProviders().length === 0}><div class="settings-card-message">{t("settings.providers.empty.noConfiguredProviders")}</div></Show>
|
||||
<div class="providers-grid">
|
||||
<For each={configuredProviders()}>{(provider) => (
|
||||
<article class="providers-card">
|
||||
<div class="providers-card-main"><div class="providers-card-mark"><ShieldCheck class="providers-card-mark-icon" /></div><div class="providers-card-copy"><div class="providers-card-title-row"><h4 class="providers-card-title">{provider.name || provider.id}</h4></div><p class="providers-card-meta">{provider.id}</p><p class="providers-card-methods">{methodSummary(provider.id)}</p><p class="providers-card-source">{describeProviderSource(provider)}</p></div></div>
|
||||
<div class="providers-card-footer"><span class="providers-model-count">{provider.modelCount === 1 ? t("settings.providers.models.one", { count: provider.modelCount }) : t("settings.providers.models.other", { count: provider.modelCount })}</span><Show when={getDisconnectMode(provider) !== "disable-in-config"}><button type="button" class="selector-button selector-button-secondary providers-disconnect-button" disabled={getDisconnectMode(provider) === "not-disconnectable"} onClick={() => void disconnectProvider(provider.id)} title={getDisconnectMode(provider) === "not-disconnectable" ? t("settings.providers.source.env") : t("settings.providers.actions.disconnect")}>{t("settings.providers.actions.disconnect")}</button></Show></div>
|
||||
</article>
|
||||
)}</For>
|
||||
</div>
|
||||
</section>
|
||||
</Show>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,23 +1,25 @@
|
|||
import { isTauriHost } from "./runtime-env"
|
||||
|
||||
export async function openExternalUrl(url: string, context = "ui"): Promise<void> {
|
||||
export async function openExternalUrl(url: string, context = "ui"): Promise<boolean> {
|
||||
if (typeof window === "undefined") {
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
if (isTauriHost()) {
|
||||
try {
|
||||
const { openUrl } = await import("@tauri-apps/plugin-opener")
|
||||
await openUrl(url)
|
||||
return
|
||||
return true
|
||||
} catch (error) {
|
||||
console.warn(`[${context}] unable to open via system opener`, error)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
window.open(url, "_blank", "noopener,noreferrer")
|
||||
const popup = window.open(url, "_blank", "noopener,noreferrer")
|
||||
return popup !== null
|
||||
} catch (error) {
|
||||
console.warn(`[${context}] unable to open external url`, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export const settingsMessages = {
|
|||
"modelSelector.favoritesOnly.showAll": "Show all models",
|
||||
"modelSelector.favorite.add": "Add to favorites",
|
||||
"modelSelector.favorite.remove": "Remove from favorites",
|
||||
"modelSelector.manageProviders": "Manage Providers",
|
||||
|
||||
"thinkingSelector.variant.default": "Default",
|
||||
"thinkingSelector.label": "Thinking: {variant}",
|
||||
|
|
@ -70,6 +71,7 @@ export const settingsMessages = {
|
|||
"settings.nav.notifications": "Notifications",
|
||||
"settings.nav.remote": "Remote Access",
|
||||
"settings.nav.speech": "Speech",
|
||||
"settings.nav.providers": "Providers",
|
||||
"settings.nav.opencode": "OpenCode",
|
||||
"settings.nav.info": "Info",
|
||||
"settings.scope.device": "This device",
|
||||
|
|
@ -127,6 +129,63 @@ export const settingsMessages = {
|
|||
"settings.opencode.logLevel.option.warn": "Warn",
|
||||
"settings.opencode.logLevel.option.error": "Error",
|
||||
|
||||
"settings.providers.title": "Provider Authentication",
|
||||
"settings.providers.subtitle": "Connect API keys or OAuth accounts for the active OpenCode instance.",
|
||||
"settings.providers.refresh": "Refresh",
|
||||
"settings.providers.selectProvider": "Select provider",
|
||||
"settings.providers.available.title": "Available Providers",
|
||||
"settings.providers.configured.title": "Configured Providers",
|
||||
"settings.providers.loading": "Loading provider authentication methods...",
|
||||
"settings.providers.empty.noInstance": "Start an OpenCode workspace before managing provider authentication.",
|
||||
"settings.providers.empty.noProviders": "No providers are available from this OpenCode instance.",
|
||||
"settings.providers.empty.noConfiguredProviders": "No providers are currently configured for this OpenCode instance.",
|
||||
"settings.providers.models.one": "{count} model",
|
||||
"settings.providers.models.other": "{count} models",
|
||||
"settings.providers.actions.connect": "Connect",
|
||||
"settings.providers.actions.disconnect": "Disconnect",
|
||||
"settings.providers.actions.remove": "Remove saved auth",
|
||||
"settings.providers.actions.close": "Close",
|
||||
"settings.providers.actions.continue": "Continue",
|
||||
"settings.providers.actions.working": "Working...",
|
||||
"settings.providers.method.title": "Authentication method",
|
||||
"settings.providers.method.subtitle": "Choose the flow OpenCode exposes for this provider.",
|
||||
"settings.providers.method.api": "API key",
|
||||
"settings.providers.method.oauth": "OAuth",
|
||||
"settings.providers.method.mixed": "API key and OAuth",
|
||||
"settings.providers.method.fallback": "API key",
|
||||
"settings.providers.status.available": "Available",
|
||||
"settings.providers.auth.title": "Connect {provider}",
|
||||
"settings.providers.auth.subtitle": "Authentication is saved inside the active OpenCode server.",
|
||||
"settings.providers.apiKey.label": "API key",
|
||||
"settings.providers.apiKey.placeholder": "Paste provider API key",
|
||||
"settings.providers.oauth.noPrompts": "This OAuth method does not need additional details before opening the browser.",
|
||||
"settings.providers.oauth.promptsUnsupported": "This OAuth method requires provider-specific prompt inputs that are not supported by the current SDK build.",
|
||||
"settings.providers.oauth.enterCode": "Complete the browser flow, then paste the authorization code.",
|
||||
"settings.providers.oauth.openPage": "Open authorization page",
|
||||
"settings.providers.oauth.openHere": "Open in this tab",
|
||||
"settings.providers.oauth.copyLink": "Copy link",
|
||||
"settings.providers.oauth.linkCopied": "Link copied",
|
||||
"settings.providers.oauth.popupBlocked": "Your browser may have blocked the authorization page. Use the actions above to continue the sign-in flow.",
|
||||
"settings.providers.oauth.popup.loadingTitle": "OpenCode authorization",
|
||||
"settings.providers.oauth.popup.loadingBody": "Preparing authorization page...",
|
||||
"settings.providers.oauth.codeLabel": "Authorization code",
|
||||
"settings.providers.oauth.codePlaceholder": "Paste code from provider",
|
||||
"settings.providers.oauth.submitCode": "Submit code",
|
||||
"settings.providers.oauth.waitingTitle": "Waiting for authorization",
|
||||
"settings.providers.oauth.cancelWait": "Cancel",
|
||||
"settings.providers.prompt.selectPlaceholder": "Select an option",
|
||||
"settings.providers.success": "Provider authentication saved. Provider data has been refreshed.",
|
||||
"settings.providers.source.api": "Saved provider authentication",
|
||||
"settings.providers.source.config": "Configured in opencode.jsonc",
|
||||
"settings.providers.source.custom": "Configured by custom provider logic",
|
||||
"settings.providers.source.env": "Configured from environment variables",
|
||||
"settings.providers.source.unknown": "Configured provider source",
|
||||
"settings.providers.errors.loadFailed": "Failed to load provider authentication methods.",
|
||||
"settings.providers.errors.noAuthorization": "Provider did not return authorization data.",
|
||||
"settings.providers.errors.authorizationFailed": "Authorization failed.",
|
||||
"settings.providers.errors.removeFailed": "Failed to remove provider authentication.",
|
||||
"settings.providers.errors.envDisconnectUnavailable": "This provider is configured from environment variables and cannot be disconnected from the UI.",
|
||||
|
||||
|
||||
"settings.appearance.behavior.title": "Interaction",
|
||||
"settings.appearance.behavior.subtitle": "Message, diff, and input defaults.",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export const settingsMessages = {
|
|||
"modelSelector.favoritesOnly.showAll": "Mostrar todos los modelos",
|
||||
"modelSelector.favorite.add": "Agregar a favoritos",
|
||||
"modelSelector.favorite.remove": "Quitar de favoritos",
|
||||
"modelSelector.manageProviders": "Manage Providers",
|
||||
|
||||
"thinkingSelector.variant.default": "Por defecto",
|
||||
"thinkingSelector.label": "Pensamiento: {variant}",
|
||||
|
|
@ -70,6 +71,7 @@ export const settingsMessages = {
|
|||
"settings.nav.notifications": "Notifications",
|
||||
"settings.nav.remote": "Remote Access",
|
||||
"settings.nav.speech": "Speech",
|
||||
"settings.nav.providers": "Providers",
|
||||
"settings.nav.opencode": "OpenCode",
|
||||
"settings.nav.info": "Info",
|
||||
"settings.scope.device": "This device",
|
||||
|
|
@ -127,6 +129,63 @@ export const settingsMessages = {
|
|||
"settings.opencode.logLevel.option.warn": "Advertencia",
|
||||
"settings.opencode.logLevel.option.error": "Error",
|
||||
|
||||
"settings.providers.title": "Provider Authentication",
|
||||
"settings.providers.subtitle": "Connect API keys or OAuth accounts for the active OpenCode instance.",
|
||||
"settings.providers.refresh": "Refresh",
|
||||
"settings.providers.selectProvider": "Select provider",
|
||||
"settings.providers.available.title": "Available Providers",
|
||||
"settings.providers.configured.title": "Configured Providers",
|
||||
"settings.providers.loading": "Loading provider authentication methods...",
|
||||
"settings.providers.empty.noInstance": "Start an OpenCode workspace before managing provider authentication.",
|
||||
"settings.providers.empty.noProviders": "No providers are available from this OpenCode instance.",
|
||||
"settings.providers.empty.noConfiguredProviders": "No providers are currently configured for this OpenCode instance.",
|
||||
"settings.providers.models.one": "{count} model",
|
||||
"settings.providers.models.other": "{count} models",
|
||||
"settings.providers.actions.connect": "Connect",
|
||||
"settings.providers.actions.disconnect": "Disconnect",
|
||||
"settings.providers.actions.remove": "Remove saved auth",
|
||||
"settings.providers.actions.close": "Close",
|
||||
"settings.providers.actions.continue": "Continue",
|
||||
"settings.providers.actions.working": "Working...",
|
||||
"settings.providers.method.title": "Authentication method",
|
||||
"settings.providers.method.subtitle": "Choose the flow OpenCode exposes for this provider.",
|
||||
"settings.providers.method.api": "API key",
|
||||
"settings.providers.method.oauth": "OAuth",
|
||||
"settings.providers.method.mixed": "API key and OAuth",
|
||||
"settings.providers.method.fallback": "API key",
|
||||
"settings.providers.status.available": "Available",
|
||||
"settings.providers.auth.title": "Connect {provider}",
|
||||
"settings.providers.auth.subtitle": "Authentication is saved inside the active OpenCode server.",
|
||||
"settings.providers.apiKey.label": "API key",
|
||||
"settings.providers.apiKey.placeholder": "Paste provider API key",
|
||||
"settings.providers.oauth.noPrompts": "This OAuth method does not need additional details before opening the browser.",
|
||||
"settings.providers.oauth.promptsUnsupported": "This OAuth method requires provider-specific prompt inputs that are not supported by the current SDK build.",
|
||||
"settings.providers.oauth.enterCode": "Complete the browser flow, then paste the authorization code.",
|
||||
"settings.providers.oauth.openPage": "Open authorization page",
|
||||
"settings.providers.oauth.openHere": "Open in this tab",
|
||||
"settings.providers.oauth.copyLink": "Copy link",
|
||||
"settings.providers.oauth.linkCopied": "Link copied",
|
||||
"settings.providers.oauth.popupBlocked": "Your browser may have blocked the authorization page. Use the actions above to continue the sign-in flow.",
|
||||
"settings.providers.oauth.popup.loadingTitle": "OpenCode authorization",
|
||||
"settings.providers.oauth.popup.loadingBody": "Preparing authorization page...",
|
||||
"settings.providers.oauth.codeLabel": "Authorization code",
|
||||
"settings.providers.oauth.codePlaceholder": "Paste code from provider",
|
||||
"settings.providers.oauth.submitCode": "Submit code",
|
||||
"settings.providers.oauth.waitingTitle": "Waiting for authorization",
|
||||
"settings.providers.oauth.cancelWait": "Cancel",
|
||||
"settings.providers.prompt.selectPlaceholder": "Select an option",
|
||||
"settings.providers.success": "Provider authentication saved. Provider data has been refreshed.",
|
||||
"settings.providers.source.api": "Saved provider authentication",
|
||||
"settings.providers.source.config": "Configured in opencode.jsonc",
|
||||
"settings.providers.source.custom": "Configured by custom provider logic",
|
||||
"settings.providers.source.env": "Configured from environment variables",
|
||||
"settings.providers.source.unknown": "Configured provider source",
|
||||
"settings.providers.errors.loadFailed": "Failed to load provider authentication methods.",
|
||||
"settings.providers.errors.noAuthorization": "Provider did not return authorization data.",
|
||||
"settings.providers.errors.authorizationFailed": "Authorization failed.",
|
||||
"settings.providers.errors.removeFailed": "Failed to remove provider authentication.",
|
||||
"settings.providers.errors.envDisconnectUnavailable": "This provider is configured from environment variables and cannot be disconnected from the UI.",
|
||||
|
||||
"settings.appearance.behavior.title": "Interaccion",
|
||||
"settings.appearance.behavior.subtitle": "Valores predeterminados de mensajes, diffs y entrada.",
|
||||
"settings.behavior.keyboardHints.title": "Sugerencias de atajos de teclado",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export const settingsMessages = {
|
|||
"modelSelector.favoritesOnly.showAll": "Afficher tous les modèles",
|
||||
"modelSelector.favorite.add": "Ajouter aux favoris",
|
||||
"modelSelector.favorite.remove": "Retirer des favoris",
|
||||
"modelSelector.manageProviders": "Manage Providers",
|
||||
|
||||
"thinkingSelector.variant.default": "Par défaut",
|
||||
"thinkingSelector.label": "Réflexion : {variant}",
|
||||
|
|
@ -70,6 +71,7 @@ export const settingsMessages = {
|
|||
"settings.nav.notifications": "Notifications",
|
||||
"settings.nav.remote": "Remote Access",
|
||||
"settings.nav.speech": "Speech",
|
||||
"settings.nav.providers": "Providers",
|
||||
"settings.nav.opencode": "OpenCode",
|
||||
"settings.nav.info": "Info",
|
||||
"settings.scope.device": "This device",
|
||||
|
|
@ -127,6 +129,63 @@ export const settingsMessages = {
|
|||
"settings.opencode.logLevel.option.warn": "Avertissement",
|
||||
"settings.opencode.logLevel.option.error": "Erreur",
|
||||
|
||||
"settings.providers.title": "Provider Authentication",
|
||||
"settings.providers.subtitle": "Connect API keys or OAuth accounts for the active OpenCode instance.",
|
||||
"settings.providers.refresh": "Refresh",
|
||||
"settings.providers.selectProvider": "Select provider",
|
||||
"settings.providers.available.title": "Available Providers",
|
||||
"settings.providers.configured.title": "Configured Providers",
|
||||
"settings.providers.loading": "Loading provider authentication methods...",
|
||||
"settings.providers.empty.noInstance": "Start an OpenCode workspace before managing provider authentication.",
|
||||
"settings.providers.empty.noProviders": "No providers are available from this OpenCode instance.",
|
||||
"settings.providers.empty.noConfiguredProviders": "No providers are currently configured for this OpenCode instance.",
|
||||
"settings.providers.models.one": "{count} model",
|
||||
"settings.providers.models.other": "{count} models",
|
||||
"settings.providers.actions.connect": "Connect",
|
||||
"settings.providers.actions.disconnect": "Disconnect",
|
||||
"settings.providers.actions.remove": "Remove saved auth",
|
||||
"settings.providers.actions.close": "Close",
|
||||
"settings.providers.actions.continue": "Continue",
|
||||
"settings.providers.actions.working": "Working...",
|
||||
"settings.providers.method.title": "Authentication method",
|
||||
"settings.providers.method.subtitle": "Choose the flow OpenCode exposes for this provider.",
|
||||
"settings.providers.method.api": "API key",
|
||||
"settings.providers.method.oauth": "OAuth",
|
||||
"settings.providers.method.mixed": "API key and OAuth",
|
||||
"settings.providers.method.fallback": "API key",
|
||||
"settings.providers.status.available": "Available",
|
||||
"settings.providers.auth.title": "Connect {provider}",
|
||||
"settings.providers.auth.subtitle": "Authentication is saved inside the active OpenCode server.",
|
||||
"settings.providers.apiKey.label": "API key",
|
||||
"settings.providers.apiKey.placeholder": "Paste provider API key",
|
||||
"settings.providers.oauth.noPrompts": "This OAuth method does not need additional details before opening the browser.",
|
||||
"settings.providers.oauth.promptsUnsupported": "This OAuth method requires provider-specific prompt inputs that are not supported by the current SDK build.",
|
||||
"settings.providers.oauth.enterCode": "Complete the browser flow, then paste the authorization code.",
|
||||
"settings.providers.oauth.openPage": "Open authorization page",
|
||||
"settings.providers.oauth.openHere": "Open in this tab",
|
||||
"settings.providers.oauth.copyLink": "Copy link",
|
||||
"settings.providers.oauth.linkCopied": "Link copied",
|
||||
"settings.providers.oauth.popupBlocked": "Your browser may have blocked the authorization page. Use the actions above to continue the sign-in flow.",
|
||||
"settings.providers.oauth.popup.loadingTitle": "OpenCode authorization",
|
||||
"settings.providers.oauth.popup.loadingBody": "Preparing authorization page...",
|
||||
"settings.providers.oauth.codeLabel": "Authorization code",
|
||||
"settings.providers.oauth.codePlaceholder": "Paste code from provider",
|
||||
"settings.providers.oauth.submitCode": "Submit code",
|
||||
"settings.providers.oauth.waitingTitle": "Waiting for authorization",
|
||||
"settings.providers.oauth.cancelWait": "Cancel",
|
||||
"settings.providers.prompt.selectPlaceholder": "Select an option",
|
||||
"settings.providers.success": "Provider authentication saved. Provider data has been refreshed.",
|
||||
"settings.providers.source.api": "Saved provider authentication",
|
||||
"settings.providers.source.config": "Configured in opencode.jsonc",
|
||||
"settings.providers.source.custom": "Configured by custom provider logic",
|
||||
"settings.providers.source.env": "Configured from environment variables",
|
||||
"settings.providers.source.unknown": "Configured provider source",
|
||||
"settings.providers.errors.loadFailed": "Failed to load provider authentication methods.",
|
||||
"settings.providers.errors.noAuthorization": "Provider did not return authorization data.",
|
||||
"settings.providers.errors.authorizationFailed": "Authorization failed.",
|
||||
"settings.providers.errors.removeFailed": "Failed to remove provider authentication.",
|
||||
"settings.providers.errors.envDisconnectUnavailable": "This provider is configured from environment variables and cannot be disconnected from the UI.",
|
||||
|
||||
"settings.appearance.behavior.title": "Interaction",
|
||||
"settings.appearance.behavior.subtitle": "Parametres par defaut pour les messages, les diffs et la saisie.",
|
||||
"settings.behavior.keyboardHints.title": "Indications de raccourcis clavier",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export const settingsMessages = {
|
|||
"modelSelector.favoritesOnly.showAll": "הצג את כל המודלים",
|
||||
"modelSelector.favorite.add": "הוסף למועדפים",
|
||||
"modelSelector.favorite.remove": "הסר ממועדפים",
|
||||
"modelSelector.manageProviders": "Manage Providers",
|
||||
|
||||
"thinkingSelector.variant.default": "ברירת מחדל",
|
||||
"thinkingSelector.label": "חשיבה: {variant}",
|
||||
|
|
@ -69,6 +70,7 @@ export const settingsMessages = {
|
|||
"settings.nav.appearance": "מראה",
|
||||
"settings.nav.notifications": "התראות",
|
||||
"settings.nav.remote": "גישה מרוחקת",
|
||||
"settings.nav.providers": "Providers",
|
||||
"settings.nav.opencode": "OpenCode",
|
||||
"settings.nav.info": "Info",
|
||||
"settings.scope.device": "מכשיר זה",
|
||||
|
|
@ -126,6 +128,63 @@ export const settingsMessages = {
|
|||
"settings.opencode.logLevel.option.warn": "אזהרה",
|
||||
"settings.opencode.logLevel.option.error": "שגיאה",
|
||||
|
||||
"settings.providers.title": "Provider Authentication",
|
||||
"settings.providers.subtitle": "Connect API keys or OAuth accounts for the active OpenCode instance.",
|
||||
"settings.providers.refresh": "Refresh",
|
||||
"settings.providers.selectProvider": "Select provider",
|
||||
"settings.providers.available.title": "Available Providers",
|
||||
"settings.providers.configured.title": "Configured Providers",
|
||||
"settings.providers.loading": "Loading provider authentication methods...",
|
||||
"settings.providers.empty.noInstance": "Start an OpenCode workspace before managing provider authentication.",
|
||||
"settings.providers.empty.noProviders": "No providers are available from this OpenCode instance.",
|
||||
"settings.providers.empty.noConfiguredProviders": "No providers are currently configured for this OpenCode instance.",
|
||||
"settings.providers.models.one": "{count} model",
|
||||
"settings.providers.models.other": "{count} models",
|
||||
"settings.providers.actions.connect": "Connect",
|
||||
"settings.providers.actions.disconnect": "Disconnect",
|
||||
"settings.providers.actions.remove": "Remove saved auth",
|
||||
"settings.providers.actions.close": "Close",
|
||||
"settings.providers.actions.continue": "Continue",
|
||||
"settings.providers.actions.working": "Working...",
|
||||
"settings.providers.method.title": "Authentication method",
|
||||
"settings.providers.method.subtitle": "Choose the flow OpenCode exposes for this provider.",
|
||||
"settings.providers.method.api": "API key",
|
||||
"settings.providers.method.oauth": "OAuth",
|
||||
"settings.providers.method.mixed": "API key and OAuth",
|
||||
"settings.providers.method.fallback": "API key",
|
||||
"settings.providers.status.available": "Available",
|
||||
"settings.providers.auth.title": "Connect {provider}",
|
||||
"settings.providers.auth.subtitle": "Authentication is saved inside the active OpenCode server.",
|
||||
"settings.providers.apiKey.label": "API key",
|
||||
"settings.providers.apiKey.placeholder": "Paste provider API key",
|
||||
"settings.providers.oauth.noPrompts": "This OAuth method does not need additional details before opening the browser.",
|
||||
"settings.providers.oauth.promptsUnsupported": "This OAuth method requires provider-specific prompt inputs that are not supported by the current SDK build.",
|
||||
"settings.providers.oauth.enterCode": "Complete the browser flow, then paste the authorization code.",
|
||||
"settings.providers.oauth.openPage": "Open authorization page",
|
||||
"settings.providers.oauth.openHere": "Open in this tab",
|
||||
"settings.providers.oauth.copyLink": "Copy link",
|
||||
"settings.providers.oauth.linkCopied": "Link copied",
|
||||
"settings.providers.oauth.popupBlocked": "Your browser may have blocked the authorization page. Use the actions above to continue the sign-in flow.",
|
||||
"settings.providers.oauth.popup.loadingTitle": "OpenCode authorization",
|
||||
"settings.providers.oauth.popup.loadingBody": "Preparing authorization page...",
|
||||
"settings.providers.oauth.codeLabel": "Authorization code",
|
||||
"settings.providers.oauth.codePlaceholder": "Paste code from provider",
|
||||
"settings.providers.oauth.submitCode": "Submit code",
|
||||
"settings.providers.oauth.waitingTitle": "Waiting for authorization",
|
||||
"settings.providers.oauth.cancelWait": "Cancel",
|
||||
"settings.providers.prompt.selectPlaceholder": "Select an option",
|
||||
"settings.providers.success": "Provider authentication saved. Provider data has been refreshed.",
|
||||
"settings.providers.source.api": "Saved provider authentication",
|
||||
"settings.providers.source.config": "Configured in opencode.jsonc",
|
||||
"settings.providers.source.custom": "Configured by custom provider logic",
|
||||
"settings.providers.source.env": "Configured from environment variables",
|
||||
"settings.providers.source.unknown": "Configured provider source",
|
||||
"settings.providers.errors.loadFailed": "Failed to load provider authentication methods.",
|
||||
"settings.providers.errors.noAuthorization": "Provider did not return authorization data.",
|
||||
"settings.providers.errors.authorizationFailed": "Authorization failed.",
|
||||
"settings.providers.errors.removeFailed": "Failed to remove provider authentication.",
|
||||
"settings.providers.errors.envDisconnectUnavailable": "This provider is configured from environment variables and cannot be disconnected from the UI.",
|
||||
|
||||
"settings.appearance.behavior.title": "אינטראקציה",
|
||||
"settings.appearance.behavior.subtitle": "ברירות מחדל להודעות, diff וקלט.",
|
||||
"settings.behavior.keyboardHints.title": "רמזי קיצורי מקלדת",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export const settingsMessages = {
|
|||
"modelSelector.favoritesOnly.showAll": "すべてのモデルを表示",
|
||||
"modelSelector.favorite.add": "お気に入りに追加",
|
||||
"modelSelector.favorite.remove": "お気に入りから削除",
|
||||
"modelSelector.manageProviders": "Manage Providers",
|
||||
|
||||
"thinkingSelector.variant.default": "デフォルト",
|
||||
"thinkingSelector.label": "思考: {variant}",
|
||||
|
|
@ -70,6 +71,7 @@ export const settingsMessages = {
|
|||
"settings.nav.notifications": "Notifications",
|
||||
"settings.nav.remote": "Remote Access",
|
||||
"settings.nav.speech": "Speech",
|
||||
"settings.nav.providers": "Providers",
|
||||
"settings.nav.opencode": "OpenCode",
|
||||
"settings.nav.info": "Info",
|
||||
"settings.scope.device": "This device",
|
||||
|
|
@ -127,6 +129,63 @@ export const settingsMessages = {
|
|||
"settings.opencode.logLevel.option.warn": "警告",
|
||||
"settings.opencode.logLevel.option.error": "エラー",
|
||||
|
||||
"settings.providers.title": "Provider Authentication",
|
||||
"settings.providers.subtitle": "Connect API keys or OAuth accounts for the active OpenCode instance.",
|
||||
"settings.providers.refresh": "Refresh",
|
||||
"settings.providers.selectProvider": "Select provider",
|
||||
"settings.providers.available.title": "Available Providers",
|
||||
"settings.providers.configured.title": "Configured Providers",
|
||||
"settings.providers.loading": "Loading provider authentication methods...",
|
||||
"settings.providers.empty.noInstance": "Start an OpenCode workspace before managing provider authentication.",
|
||||
"settings.providers.empty.noProviders": "No providers are available from this OpenCode instance.",
|
||||
"settings.providers.empty.noConfiguredProviders": "No providers are currently configured for this OpenCode instance.",
|
||||
"settings.providers.models.one": "{count} model",
|
||||
"settings.providers.models.other": "{count} models",
|
||||
"settings.providers.actions.connect": "Connect",
|
||||
"settings.providers.actions.disconnect": "Disconnect",
|
||||
"settings.providers.actions.remove": "Remove saved auth",
|
||||
"settings.providers.actions.close": "Close",
|
||||
"settings.providers.actions.continue": "Continue",
|
||||
"settings.providers.actions.working": "Working...",
|
||||
"settings.providers.method.title": "Authentication method",
|
||||
"settings.providers.method.subtitle": "Choose the flow OpenCode exposes for this provider.",
|
||||
"settings.providers.method.api": "API key",
|
||||
"settings.providers.method.oauth": "OAuth",
|
||||
"settings.providers.method.mixed": "API key and OAuth",
|
||||
"settings.providers.method.fallback": "API key",
|
||||
"settings.providers.status.available": "Available",
|
||||
"settings.providers.auth.title": "Connect {provider}",
|
||||
"settings.providers.auth.subtitle": "Authentication is saved inside the active OpenCode server.",
|
||||
"settings.providers.apiKey.label": "API key",
|
||||
"settings.providers.apiKey.placeholder": "Paste provider API key",
|
||||
"settings.providers.oauth.noPrompts": "This OAuth method does not need additional details before opening the browser.",
|
||||
"settings.providers.oauth.promptsUnsupported": "This OAuth method requires provider-specific prompt inputs that are not supported by the current SDK build.",
|
||||
"settings.providers.oauth.enterCode": "Complete the browser flow, then paste the authorization code.",
|
||||
"settings.providers.oauth.openPage": "Open authorization page",
|
||||
"settings.providers.oauth.openHere": "Open in this tab",
|
||||
"settings.providers.oauth.copyLink": "Copy link",
|
||||
"settings.providers.oauth.linkCopied": "Link copied",
|
||||
"settings.providers.oauth.popupBlocked": "Your browser may have blocked the authorization page. Use the actions above to continue the sign-in flow.",
|
||||
"settings.providers.oauth.popup.loadingTitle": "OpenCode authorization",
|
||||
"settings.providers.oauth.popup.loadingBody": "Preparing authorization page...",
|
||||
"settings.providers.oauth.codeLabel": "Authorization code",
|
||||
"settings.providers.oauth.codePlaceholder": "Paste code from provider",
|
||||
"settings.providers.oauth.submitCode": "Submit code",
|
||||
"settings.providers.oauth.waitingTitle": "Waiting for authorization",
|
||||
"settings.providers.oauth.cancelWait": "Cancel",
|
||||
"settings.providers.prompt.selectPlaceholder": "Select an option",
|
||||
"settings.providers.success": "Provider authentication saved. Provider data has been refreshed.",
|
||||
"settings.providers.source.api": "Saved provider authentication",
|
||||
"settings.providers.source.config": "Configured in opencode.jsonc",
|
||||
"settings.providers.source.custom": "Configured by custom provider logic",
|
||||
"settings.providers.source.env": "Configured from environment variables",
|
||||
"settings.providers.source.unknown": "Configured provider source",
|
||||
"settings.providers.errors.loadFailed": "Failed to load provider authentication methods.",
|
||||
"settings.providers.errors.noAuthorization": "Provider did not return authorization data.",
|
||||
"settings.providers.errors.authorizationFailed": "Authorization failed.",
|
||||
"settings.providers.errors.removeFailed": "Failed to remove provider authentication.",
|
||||
"settings.providers.errors.envDisconnectUnavailable": "This provider is configured from environment variables and cannot be disconnected from the UI.",
|
||||
|
||||
"settings.appearance.behavior.title": "操作",
|
||||
"settings.appearance.behavior.subtitle": "メッセージ、差分、入力の既定値。",
|
||||
"settings.behavior.keyboardHints.title": "キーボードショートカットのヒント",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export const settingsMessages = {
|
|||
"modelSelector.favoritesOnly.showAll": "Показать все модели",
|
||||
"modelSelector.favorite.add": "Добавить в избранное",
|
||||
"modelSelector.favorite.remove": "Удалить из избранного",
|
||||
"modelSelector.manageProviders": "Manage Providers",
|
||||
|
||||
"thinkingSelector.variant.default": "По умолчанию",
|
||||
"thinkingSelector.label": "Размышления: {variant}",
|
||||
|
|
@ -70,6 +71,7 @@ export const settingsMessages = {
|
|||
"settings.nav.notifications": "Notifications",
|
||||
"settings.nav.remote": "Remote Access",
|
||||
"settings.nav.speech": "Speech",
|
||||
"settings.nav.providers": "Providers",
|
||||
"settings.nav.opencode": "OpenCode",
|
||||
"settings.nav.info": "Info",
|
||||
"settings.scope.device": "This device",
|
||||
|
|
@ -127,6 +129,63 @@ export const settingsMessages = {
|
|||
"settings.opencode.logLevel.option.warn": "Предупреждение",
|
||||
"settings.opencode.logLevel.option.error": "Ошибка",
|
||||
|
||||
"settings.providers.title": "Provider Authentication",
|
||||
"settings.providers.subtitle": "Connect API keys or OAuth accounts for the active OpenCode instance.",
|
||||
"settings.providers.refresh": "Refresh",
|
||||
"settings.providers.selectProvider": "Select provider",
|
||||
"settings.providers.available.title": "Available Providers",
|
||||
"settings.providers.configured.title": "Configured Providers",
|
||||
"settings.providers.loading": "Loading provider authentication methods...",
|
||||
"settings.providers.empty.noInstance": "Start an OpenCode workspace before managing provider authentication.",
|
||||
"settings.providers.empty.noProviders": "No providers are available from this OpenCode instance.",
|
||||
"settings.providers.empty.noConfiguredProviders": "No providers are currently configured for this OpenCode instance.",
|
||||
"settings.providers.models.one": "{count} model",
|
||||
"settings.providers.models.other": "{count} models",
|
||||
"settings.providers.actions.connect": "Connect",
|
||||
"settings.providers.actions.disconnect": "Disconnect",
|
||||
"settings.providers.actions.remove": "Remove saved auth",
|
||||
"settings.providers.actions.close": "Close",
|
||||
"settings.providers.actions.continue": "Continue",
|
||||
"settings.providers.actions.working": "Working...",
|
||||
"settings.providers.method.title": "Authentication method",
|
||||
"settings.providers.method.subtitle": "Choose the flow OpenCode exposes for this provider.",
|
||||
"settings.providers.method.api": "API key",
|
||||
"settings.providers.method.oauth": "OAuth",
|
||||
"settings.providers.method.mixed": "API key and OAuth",
|
||||
"settings.providers.method.fallback": "API key",
|
||||
"settings.providers.status.available": "Available",
|
||||
"settings.providers.auth.title": "Connect {provider}",
|
||||
"settings.providers.auth.subtitle": "Authentication is saved inside the active OpenCode server.",
|
||||
"settings.providers.apiKey.label": "API key",
|
||||
"settings.providers.apiKey.placeholder": "Paste provider API key",
|
||||
"settings.providers.oauth.noPrompts": "This OAuth method does not need additional details before opening the browser.",
|
||||
"settings.providers.oauth.promptsUnsupported": "This OAuth method requires provider-specific prompt inputs that are not supported by the current SDK build.",
|
||||
"settings.providers.oauth.enterCode": "Complete the browser flow, then paste the authorization code.",
|
||||
"settings.providers.oauth.openPage": "Open authorization page",
|
||||
"settings.providers.oauth.openHere": "Open in this tab",
|
||||
"settings.providers.oauth.copyLink": "Copy link",
|
||||
"settings.providers.oauth.linkCopied": "Link copied",
|
||||
"settings.providers.oauth.popupBlocked": "Your browser may have blocked the authorization page. Use the actions above to continue the sign-in flow.",
|
||||
"settings.providers.oauth.popup.loadingTitle": "OpenCode authorization",
|
||||
"settings.providers.oauth.popup.loadingBody": "Preparing authorization page...",
|
||||
"settings.providers.oauth.codeLabel": "Authorization code",
|
||||
"settings.providers.oauth.codePlaceholder": "Paste code from provider",
|
||||
"settings.providers.oauth.submitCode": "Submit code",
|
||||
"settings.providers.oauth.waitingTitle": "Waiting for authorization",
|
||||
"settings.providers.oauth.cancelWait": "Cancel",
|
||||
"settings.providers.prompt.selectPlaceholder": "Select an option",
|
||||
"settings.providers.success": "Provider authentication saved. Provider data has been refreshed.",
|
||||
"settings.providers.source.api": "Saved provider authentication",
|
||||
"settings.providers.source.config": "Configured in opencode.jsonc",
|
||||
"settings.providers.source.custom": "Configured by custom provider logic",
|
||||
"settings.providers.source.env": "Configured from environment variables",
|
||||
"settings.providers.source.unknown": "Configured provider source",
|
||||
"settings.providers.errors.loadFailed": "Failed to load provider authentication methods.",
|
||||
"settings.providers.errors.noAuthorization": "Provider did not return authorization data.",
|
||||
"settings.providers.errors.authorizationFailed": "Authorization failed.",
|
||||
"settings.providers.errors.removeFailed": "Failed to remove provider authentication.",
|
||||
"settings.providers.errors.envDisconnectUnavailable": "This provider is configured from environment variables and cannot be disconnected from the UI.",
|
||||
|
||||
"settings.appearance.behavior.title": "Взаимодействие",
|
||||
"settings.appearance.behavior.subtitle": "Значения по умолчанию для сообщений, диффов и ввода.",
|
||||
"settings.behavior.keyboardHints.title": "Подсказки сочетаний клавиш",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export const settingsMessages = {
|
|||
"modelSelector.favoritesOnly.showAll": "显示所有模型",
|
||||
"modelSelector.favorite.add": "添加到收藏",
|
||||
"modelSelector.favorite.remove": "从收藏移除",
|
||||
"modelSelector.manageProviders": "Manage Providers",
|
||||
|
||||
"thinkingSelector.variant.default": "默认",
|
||||
"thinkingSelector.label": "思考:{variant}",
|
||||
|
|
@ -70,6 +71,7 @@ export const settingsMessages = {
|
|||
"settings.nav.notifications": "Notifications",
|
||||
"settings.nav.remote": "Remote Access",
|
||||
"settings.nav.speech": "Speech",
|
||||
"settings.nav.providers": "Providers",
|
||||
"settings.nav.opencode": "OpenCode",
|
||||
"settings.nav.info": "Info",
|
||||
"settings.scope.device": "This device",
|
||||
|
|
@ -127,6 +129,63 @@ export const settingsMessages = {
|
|||
"settings.opencode.logLevel.option.warn": "警告",
|
||||
"settings.opencode.logLevel.option.error": "错误",
|
||||
|
||||
"settings.providers.title": "Provider Authentication",
|
||||
"settings.providers.subtitle": "Connect API keys or OAuth accounts for the active OpenCode instance.",
|
||||
"settings.providers.refresh": "Refresh",
|
||||
"settings.providers.selectProvider": "Select provider",
|
||||
"settings.providers.available.title": "Available Providers",
|
||||
"settings.providers.configured.title": "Configured Providers",
|
||||
"settings.providers.loading": "Loading provider authentication methods...",
|
||||
"settings.providers.empty.noInstance": "Start an OpenCode workspace before managing provider authentication.",
|
||||
"settings.providers.empty.noProviders": "No providers are available from this OpenCode instance.",
|
||||
"settings.providers.empty.noConfiguredProviders": "No providers are currently configured for this OpenCode instance.",
|
||||
"settings.providers.models.one": "{count} model",
|
||||
"settings.providers.models.other": "{count} models",
|
||||
"settings.providers.actions.connect": "Connect",
|
||||
"settings.providers.actions.disconnect": "Disconnect",
|
||||
"settings.providers.actions.remove": "Remove saved auth",
|
||||
"settings.providers.actions.close": "Close",
|
||||
"settings.providers.actions.continue": "Continue",
|
||||
"settings.providers.actions.working": "Working...",
|
||||
"settings.providers.method.title": "Authentication method",
|
||||
"settings.providers.method.subtitle": "Choose the flow OpenCode exposes for this provider.",
|
||||
"settings.providers.method.api": "API key",
|
||||
"settings.providers.method.oauth": "OAuth",
|
||||
"settings.providers.method.mixed": "API key and OAuth",
|
||||
"settings.providers.method.fallback": "API key",
|
||||
"settings.providers.status.available": "Available",
|
||||
"settings.providers.auth.title": "Connect {provider}",
|
||||
"settings.providers.auth.subtitle": "Authentication is saved inside the active OpenCode server.",
|
||||
"settings.providers.apiKey.label": "API key",
|
||||
"settings.providers.apiKey.placeholder": "Paste provider API key",
|
||||
"settings.providers.oauth.noPrompts": "This OAuth method does not need additional details before opening the browser.",
|
||||
"settings.providers.oauth.promptsUnsupported": "This OAuth method requires provider-specific prompt inputs that are not supported by the current SDK build.",
|
||||
"settings.providers.oauth.enterCode": "Complete the browser flow, then paste the authorization code.",
|
||||
"settings.providers.oauth.openPage": "Open authorization page",
|
||||
"settings.providers.oauth.openHere": "Open in this tab",
|
||||
"settings.providers.oauth.copyLink": "Copy link",
|
||||
"settings.providers.oauth.linkCopied": "Link copied",
|
||||
"settings.providers.oauth.popupBlocked": "Your browser may have blocked the authorization page. Use the actions above to continue the sign-in flow.",
|
||||
"settings.providers.oauth.popup.loadingTitle": "OpenCode authorization",
|
||||
"settings.providers.oauth.popup.loadingBody": "Preparing authorization page...",
|
||||
"settings.providers.oauth.codeLabel": "Authorization code",
|
||||
"settings.providers.oauth.codePlaceholder": "Paste code from provider",
|
||||
"settings.providers.oauth.submitCode": "Submit code",
|
||||
"settings.providers.oauth.waitingTitle": "Waiting for authorization",
|
||||
"settings.providers.oauth.cancelWait": "Cancel",
|
||||
"settings.providers.prompt.selectPlaceholder": "Select an option",
|
||||
"settings.providers.success": "Provider authentication saved. Provider data has been refreshed.",
|
||||
"settings.providers.source.api": "Saved provider authentication",
|
||||
"settings.providers.source.config": "Configured in opencode.jsonc",
|
||||
"settings.providers.source.custom": "Configured by custom provider logic",
|
||||
"settings.providers.source.env": "Configured from environment variables",
|
||||
"settings.providers.source.unknown": "Configured provider source",
|
||||
"settings.providers.errors.loadFailed": "Failed to load provider authentication methods.",
|
||||
"settings.providers.errors.noAuthorization": "Provider did not return authorization data.",
|
||||
"settings.providers.errors.authorizationFailed": "Authorization failed.",
|
||||
"settings.providers.errors.removeFailed": "Failed to remove provider authentication.",
|
||||
"settings.providers.errors.envDisconnectUnavailable": "This provider is configured from environment variables and cannot be disconnected from the UI.",
|
||||
|
||||
"settings.appearance.behavior.title": "交互",
|
||||
"settings.appearance.behavior.subtitle": "消息、差异与输入的默认值。",
|
||||
"settings.behavior.keyboardHints.title": "键盘快捷键提示",
|
||||
|
|
|
|||
58
packages/ui/src/lib/provider-auth.ts
Normal file
58
packages/ui/src/lib/provider-auth.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
export type ProviderAuthMethod = {
|
||||
type: "oauth" | "api"
|
||||
label: string
|
||||
prompts?: ProviderAuthPrompt[]
|
||||
}
|
||||
|
||||
export type ProviderAuthPrompt =
|
||||
| {
|
||||
type: "text"
|
||||
key: string
|
||||
message: string
|
||||
placeholder?: string
|
||||
when?: ProviderAuthPromptCondition
|
||||
}
|
||||
| {
|
||||
type: "select"
|
||||
key: string
|
||||
message: string
|
||||
options: Array<{ label: string; value: string; hint?: string }>
|
||||
when?: ProviderAuthPromptCondition
|
||||
}
|
||||
|
||||
export type ProviderAuthPromptCondition = {
|
||||
key: string
|
||||
op: "eq" | "neq"
|
||||
value: string
|
||||
}
|
||||
|
||||
export type ProviderAuthAuthorization = {
|
||||
url: string
|
||||
method: "auto" | "code"
|
||||
instructions: string
|
||||
}
|
||||
|
||||
export const genericApiMethod: ProviderAuthMethod = { type: "api", label: "" }
|
||||
|
||||
export function extractProviderAuthErrorMessage(error: unknown, fallback: string): string {
|
||||
const candidate = error as {
|
||||
data?: { message?: unknown }
|
||||
message?: unknown
|
||||
error?: { data?: { message?: unknown }; message?: unknown }
|
||||
}
|
||||
const nested = candidate?.error
|
||||
const message = candidate?.data?.message ?? nested?.data?.message ?? candidate?.message ?? nested?.message
|
||||
return typeof message === "string" && message.trim().length > 0 ? message : fallback
|
||||
}
|
||||
|
||||
export function shouldShowProviderAuthPrompt(prompt: ProviderAuthPrompt, values: Record<string, string>): boolean {
|
||||
if (!prompt.when) return true
|
||||
const actual = values[prompt.when.key]
|
||||
if (actual === undefined) return false
|
||||
return prompt.when.op === "eq" ? actual === prompt.when.value : actual !== prompt.when.value
|
||||
}
|
||||
|
||||
export function isAbortError(error: unknown): boolean {
|
||||
const candidate = error as { name?: unknown; message?: unknown }
|
||||
return candidate?.name === "AbortError" || candidate?.message === "This operation was aborted"
|
||||
}
|
||||
|
|
@ -1,6 +1,13 @@
|
|||
import { createSignal } from "solid-js"
|
||||
|
||||
export type SettingsSectionId = "appearance" | "notifications" | "remote" | "speech" | "opencode" | "sidecars" | "info"
|
||||
export type SettingsSectionId =
|
||||
| "appearance"
|
||||
| "notifications"
|
||||
| "remote"
|
||||
| "speech"
|
||||
| "opencode"
|
||||
| "sidecars"
|
||||
| "info"
|
||||
|
||||
const [settingsOpen, setSettingsOpen] = createSignal(false)
|
||||
const [activeSettingsSection, setActiveSettingsSection] = createSignal<SettingsSectionId>("appearance")
|
||||
|
|
|
|||
387
packages/ui/src/styles/components/provider-auth.css
Normal file
387
packages/ui/src/styles/components/provider-auth.css
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
.providers-manager-modal {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
z-index: 60;
|
||||
transform: translate(-50%, -50%);
|
||||
width: min(880px, calc(100vw - 2rem));
|
||||
max-height: min(860px, calc(100dvh - 2rem));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.providers-manager-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--border-base);
|
||||
}
|
||||
|
||||
.providers-manager-title {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.providers-manager-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.providers-connect-bar {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
padding: 0.875rem;
|
||||
border: 1px solid var(--border-base);
|
||||
border-radius: 0;
|
||||
background: color-mix(in oklab, var(--surface-base) 84%, var(--surface-secondary) 16%);
|
||||
}
|
||||
|
||||
.providers-connect-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.providers-connect-panel,
|
||||
.providers-list-section {
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border-base);
|
||||
border-radius: 0;
|
||||
background: color-mix(in oklab, var(--surface-base) 88%, var(--surface-secondary) 12%);
|
||||
}
|
||||
|
||||
.providers-panel-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid color-mix(in oklab, var(--border-base) 70%, transparent);
|
||||
}
|
||||
|
||||
.providers-overview-card,
|
||||
.providers-auth-card {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.providers-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 0.875rem;
|
||||
}
|
||||
|
||||
.providers-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
min-height: 13rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border-base);
|
||||
border-radius: 0;
|
||||
background:
|
||||
linear-gradient(135deg, color-mix(in oklab, var(--surface-base) 84%, var(--accent-primary) 5%), var(--surface-base));
|
||||
transition: border-color 140ms ease, background-color 140ms ease, transform 140ms ease;
|
||||
}
|
||||
|
||||
.providers-card:hover,
|
||||
.providers-card[data-active="true"] {
|
||||
border-color: color-mix(in oklab, var(--accent-primary) 34%, var(--border-base));
|
||||
background: color-mix(in oklab, var(--accent-primary) 8%, var(--surface-base));
|
||||
}
|
||||
|
||||
.providers-card[data-active="true"] {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.providers-card-main {
|
||||
display: flex;
|
||||
gap: 0.875rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.providers-card-mark {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.35rem;
|
||||
height: 2.35rem;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid color-mix(in oklab, var(--accent-primary) 22%, var(--border-base));
|
||||
border-radius: 0;
|
||||
background: color-mix(in oklab, var(--accent-primary) 12%, var(--surface-base));
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.providers-card-mark-icon,
|
||||
.providers-button-icon,
|
||||
.providers-input-icon,
|
||||
.providers-instructions-icon,
|
||||
.providers-success-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.providers-card-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.providers-card-title {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.providers-card-title-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.providers-connected-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border: 1px solid color-mix(in oklab, var(--accent-primary) 28%, var(--border-base));
|
||||
border-radius: 0;
|
||||
background: color-mix(in oklab, var(--accent-primary) 10%, var(--surface-base));
|
||||
color: var(--accent-primary);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.providers-card-meta,
|
||||
.providers-card-methods,
|
||||
.providers-model-count,
|
||||
.providers-select-hint {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.providers-card-source {
|
||||
margin-top: 0.35rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
.providers-card-meta {
|
||||
margin-top: 0.2rem;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.providers-card-methods {
|
||||
margin-top: 0.75rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.providers-card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.25rem;
|
||||
padding-top: 0.875rem;
|
||||
border-top: 1px solid color-mix(in oklab, var(--border-base) 70%, transparent);
|
||||
}
|
||||
|
||||
.providers-disconnect-button {
|
||||
width: min(9rem, 50%);
|
||||
min-width: 6.5rem;
|
||||
justify-content: center;
|
||||
padding-inline: 0.75rem;
|
||||
}
|
||||
|
||||
.providers-loading-row,
|
||||
.providers-waiting-card,
|
||||
.providers-success-card,
|
||||
.providers-oauth-instructions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.875rem;
|
||||
border: 1px solid var(--border-base);
|
||||
border-radius: 0;
|
||||
background: var(--surface-base);
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.providers-oauth-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.providers-oauth-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.providers-waiting-card {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.providers-wait-cancel {
|
||||
margin-inline-start: auto;
|
||||
flex-shrink: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.providers-success-card {
|
||||
border-color: color-mix(in oklab, var(--accent-primary) 34%, var(--border-base));
|
||||
background: color-mix(in oklab, var(--accent-primary) 10%, var(--surface-base));
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.providers-success-icon {
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.providers-form-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.providers-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.providers-input-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.providers-input-icon {
|
||||
position: absolute;
|
||||
inset-inline-start: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.providers-input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 0.65rem 0.75rem;
|
||||
border: 1px solid var(--border-base);
|
||||
border-radius: 0;
|
||||
background: var(--surface-base);
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.providers-input-wrap .providers-input {
|
||||
padding-inline-start: 2.25rem;
|
||||
}
|
||||
|
||||
.providers-input:focus {
|
||||
border-color: color-mix(in oklab, var(--accent-primary) 48%, var(--border-base));
|
||||
box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent-primary) 16%, transparent);
|
||||
}
|
||||
|
||||
.providers-method-row {
|
||||
margin-bottom: 1rem;
|
||||
padding-top: 0;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.providers-method-trigger,
|
||||
.providers-prompt-trigger {
|
||||
min-width: min(22rem, 100%);
|
||||
}
|
||||
|
||||
.providers-select-hint {
|
||||
margin-inline-start: 0.5rem;
|
||||
}
|
||||
|
||||
.providers-actions-row {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.providers-spin-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
animation: providers-spin 900ms linear infinite;
|
||||
}
|
||||
|
||||
@keyframes providers-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.providers-manager-modal {
|
||||
width: 100vw;
|
||||
max-height: 100dvh;
|
||||
height: 100dvh;
|
||||
}
|
||||
|
||||
.providers-connect-bar,
|
||||
.providers-panel-header {
|
||||
grid-template-columns: 1fr;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.providers-connect-bar .selector-button,
|
||||
.providers-connect-bar .settings-pill-button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.providers-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.providers-card-footer,
|
||||
.providers-actions-row {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.providers-oauth-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.providers-waiting-card {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.providers-wait-cancel {
|
||||
margin-inline-start: 0;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.providers-card-footer .selector-button,
|
||||
.providers-actions-row .selector-button,
|
||||
.providers-method-trigger,
|
||||
.providers-prompt-trigger {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.providers-disconnect-button {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -11,3 +11,4 @@
|
|||
@import "./components/permission-notification.css";
|
||||
@import "./components/settings-screen.css";
|
||||
@import "./components/settings-info.css";
|
||||
@import "./components/provider-auth.css";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue