From a35cf908794d34241944c1e8e260244432be110d Mon Sep 17 00:00:00 2001 From: akemmanuel Date: Mon, 25 May 2026 14:15:43 -0300 Subject: [PATCH 1/3] fix provider auth bridge calls and implement provider settings view - Fix setProviderAuth parameter shape to match SDK AuthSetData - Fix removeProviderAuth to call DELETE /auth/{id} via raw client - Implement SettingsProviders with search, overlay dialogs, disconnect confirmation - Create DialogSelectProvider showing all providers with connection checkmarks - Create DialogCustomProvider with optional model fields --- opencode-bridge.ts | 7 +- src/components/DialogCustomProvider.tsx | 3 +- src/components/DialogSelectProvider.tsx | 16 +- src/components/SettingsProviders.tsx | 534 +++++++++++++++++------- 4 files changed, 394 insertions(+), 166 deletions(-) diff --git a/opencode-bridge.ts b/opencode-bridge.ts index 06c1b1d..99d8ac0 100644 --- a/opencode-bridge.ts +++ b/opencode-bridge.ts @@ -390,13 +390,16 @@ class OpenCodeConnection { async setProviderAuth(providerID, auth) { this._requireClient(); - const res = await this._client.auth.set({ providerID, auth }); + const res = await this._client.auth.set({ path: { id: providerID }, body: auth }); return res.data; } async removeProviderAuth(providerID) { this._requireClient(); - const res = await this._client.auth.remove({ providerID }); + const res = await this._client.auth._client.delete({ + path: { id: providerID }, + url: "/auth/{id}", + }); return res.data; } diff --git a/src/components/DialogCustomProvider.tsx b/src/components/DialogCustomProvider.tsx index a7e2da4..0c082c9 100644 --- a/src/components/DialogCustomProvider.tsx +++ b/src/components/DialogCustomProvider.tsx @@ -143,7 +143,6 @@ function validate( if (!baseUrl.trim()) return "Base URL is required"; if (!baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")) return "Base URL must start with http:// or https://"; - if (models.length === 0) return "At least one model is required"; for (const m of models) { if (!m.id.trim()) return "All model IDs must be filled in"; if (!m.name.trim()) return "All model names must be filled in"; @@ -353,7 +352,7 @@ export function DialogCustomProvider({ firstPlaceholder="model-id" secondPlaceholder="Display Name" firstClassName="font-mono" - minEntries={1} + minEntries={0} onAdd={addModel} onRemove={removeModel} onUpdate={(idx, field, value) => updateModel(idx, field === "first" ? "id" : "name", value)} diff --git a/src/components/DialogSelectProvider.tsx b/src/components/DialogSelectProvider.tsx index 56cf219..a186fd2 100644 --- a/src/components/DialogSelectProvider.tsx +++ b/src/components/DialogSelectProvider.tsx @@ -49,8 +49,6 @@ export function DialogSelectProvider({ const pop: Provider[] = []; const oth: Provider[] = []; for (const p of providers) { - // Skip already connected - if (connectedIds.has(p.id)) continue; // Filter by search if ( lowerSearch && @@ -113,7 +111,12 @@ export function DialogSelectProvider({ Popular {popular.map((p) => ( - + ))} )} @@ -125,7 +128,12 @@ export function DialogSelectProvider({ Other {other.map((p) => ( - + ))} )} diff --git a/src/components/SettingsProviders.tsx b/src/components/SettingsProviders.tsx index a3f0b13..423ff16 100644 --- a/src/components/SettingsProviders.tsx +++ b/src/components/SettingsProviders.tsx @@ -9,22 +9,22 @@ import type { AgentBackendId } from "@/agents"; import { AGENT_BACKEND_LABELS } from "@/agents"; -import { Loader2, Plus, Unplug } from "lucide-react"; -import { useCallback, useEffect, useState } from "react"; +import { Loader2, Plus, Search, Unplug } from "lucide-react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { DialogConnectProvider } from "@/components/DialogConnectProvider"; import { DialogCustomProvider } from "@/components/DialogCustomProvider"; import { DialogSelectProvider } from "@/components/DialogSelectProvider"; import { ProviderIcon } from "@/components/provider-icons/ProviderIcon"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; import { Spinner } from "@/components/ui/spinner"; -import { - useAgentBackend, - useAvailableBackendIds, - useCurrentAgentBackendId, -} from "@/hooks/use-agent-backend"; +import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; +import { useAgentBackend, useCurrentAgentBackendId } from "@/hooks/use-agent-backend"; import { useActions, useConnectionState } from "@/hooks/use-agent-state"; +import { useOpenGuiClient } from "@/protocol/provider"; import { POPULAR_PROVIDER_IDS } from "@/lib/constants"; +import { getErrorMessage } from "@/lib/utils"; import type { AllProvidersData, ProviderAuthMethod } from "@/types/electron"; // --------------------------------------------------------------------------- @@ -57,7 +57,6 @@ export function SettingsProviders() { const { refreshProviders } = useActions(); const { activeDirectory, activeWorkspaceId } = useConnectionState(); const initialBackendId = useCurrentAgentBackendId(); - const availableBackendIds = useAvailableBackendIds(); const [backendId, setBackendId] = useState(initialBackendId); const backend = useAgentBackend(backendId); const providersApi = backend?.platform?.providers; @@ -68,18 +67,37 @@ export function SettingsProviders() { const [authMethods, setAuthMethods] = useState>({}); const [loading, setLoading] = useState(true); const [disconnecting, setDisconnecting] = useState(null); + const [confirmingDisconnect, setConfirmingDisconnect] = useState(null); + const [disconnectError, setDisconnectError] = useState(null); // Sub-dialog state const [connectProviderID, setConnectProviderID] = useState(null); const [showCustom, setShowCustom] = useState(false); const [showSelectAll, setShowSelectAll] = useState(false); + // Search + const [search, setSearch] = useState(""); + const lowerSearch = search.toLowerCase().trim(); + const isSearching = lowerSearch.length > 0; + + // Filter backends that support provider management + const openGuiClient = useOpenGuiClient(); + const providerBackendIds = useMemo( + () => + openGuiClient.agentBackends + .list() + .filter((b) => b.capabilities?.providerAuth) + .map((b) => b.id as AgentBackendId), + [openGuiClient], + ); + // Wait for auth methods to be loaded for this provider const isAuthLoading = loading || (!connectProviderID ? false : authMethods[connectProviderID] === undefined); const refresh = useCallback(async () => { if (!providersApi) return; + setLoading(true); const target = { directory: scopedDirectory, workspaceId: activeWorkspaceId }; const [allProvidersData, providerAuthMethods] = await Promise.all([ providersApi.listAll(target), @@ -96,6 +114,8 @@ export function SettingsProviders() { const handleDisconnect = async (providerID: string) => { if (!providersApi) return; + setConfirmingDisconnect(null); + setDisconnectError(null); setDisconnecting(providerID); try { const target = { directory: scopedDirectory, workspaceId: activeWorkspaceId }; @@ -103,6 +123,8 @@ export function SettingsProviders() { await providersApi.dispose(target); await refresh(); await refreshProviders(); + } catch (err) { + setDisconnectError(getErrorMessage(err, "Failed to disconnect")); } finally { setDisconnecting(null); } @@ -144,169 +166,365 @@ export function SettingsProviders() { const providerList = Array.isArray(allProviders.all) ? allProviders.all : []; const connectedIds = Array.isArray(allProviders.connected) ? allProviders.connected : []; const connectedSet = new Set(connectedIds); - const connectedProviders = providerList.filter((p) => connectedSet.has(p.id)); + const connectedProviders = providerList + .filter((p) => connectedSet.has(p.id)) + .sort((a, b) => (a.name || a.id).localeCompare(b.name || b.id)); const popularNotConnected = POPULAR_PROVIDER_IDS.filter((id) => !connectedSet.has(id)); - // For popular providers that aren't in the `all` list (not yet fetched from server), - // create a minimal entry const allById = new Map(providerList.map((p) => [p.id, p])); - // If a connect dialog is open, show it instead - if (connectProviderID) { - const provider = allById.get(connectProviderID); - return ( - setConnectProviderID(null)} - /> - ); - } - - if (showCustom) { - return ( - setShowCustom(false)} - /> - ); - } - - if (showSelectAll) { - return ( - { - setShowSelectAll(false); - setConnectProviderID(id); - }} - onCustom={() => { - setShowSelectAll(false); - setShowCustom(true); - }} - onBack={() => setShowSelectAll(false)} - /> - ); - } + const connectProvider = connectProviderID ? allById.get(connectProviderID) : null; return ( -
- {availableBackendIds.length > 1 && ( -
- {availableBackendIds.map((id) => ( - - ))} -
- )} - {/* Connected providers */} - {connectedProviders.length > 0 && ( -
-

- Connected -

- {connectedProviders.map((provider) => { - const isEnv = provider.source === "env"; - const isDisconnecting = disconnecting === provider.id; - return ( -
+
+ {providerBackendIds.length > 1 && ( +
+ {providerBackendIds.map((id) => ( + + ))} +
+ )} + {/* Search */} +
+ + setSearch(e.target.value)} + placeholder="Search providers..." + className="pl-8 text-sm" + /> +
+ {isSearching ? ( +
+ {providerList + .filter( + (p) => + p.id.toLowerCase().includes(lowerSearch) || + (p.name || "").toLowerCase().includes(lowerSearch), + ) + .sort((a, b) => (a.name || a.id).localeCompare(b.name || b.id)) + .map((provider) => { + const isConnected = connectedSet.has(provider.id); + const isEnv = provider.source === "env"; + const isDisconnecting = disconnecting === provider.id; + const isConfirming = confirmingDisconnect === provider.id; + const showError = disconnectError && !isDisconnecting; + return ( +
+
+ + + {provider.name || provider.id} + + {isConnected ? ( + isEnv ? ( + + from env + + ) : isConfirming ? ( +
+ + Disconnect {provider.name || provider.id}? + + + +
+ ) : ( + + ) + ) : ( + + )} +
+ {showError && ( +
+

{disconnectError}

+ +
)} - Disconnect - - )} +
+ ); + })} + {providerList.filter( + (p) => + p.id.toLowerCase().includes(lowerSearch) || + (p.name || "").toLowerCase().includes(lowerSearch), + ).length === 0 && ( +
+ No providers found for "{search}"
- ); - })} -
- )} + )} +
+ ) : ( + <> + {connectedProviders.length > 0 && ( +
+

+ Connected +

+ {connectedProviders.map((provider) => { + const isEnv = provider.source === "env"; + const isDisconnecting = disconnecting === provider.id; + const isConfirming = confirmingDisconnect === provider.id; + const showError = disconnectError && !isDisconnecting; + return ( +
+
+ +
+
+ + {provider.name || provider.id} + + +
+
+ {isEnv ? ( + + from env + + ) : isConfirming ? ( +
+ + Disconnect {provider.name || provider.id}? + + + +
+ ) : ( + + )} +
+ {showError && ( +
+

{disconnectError}

+ +
+ )} +
+ ); + })} +
+ )} + + {/* Popular providers (not yet connected) */} + {popularNotConnected.length > 0 && ( +
+

+ Popular +

+ {popularNotConnected.map((id) => { + const provider = allById.get(id); + return ( +
+ + + {provider?.name || id} + + +
+ ); + })} +
+ )} - {/* Popular providers (not yet connected) */} - {popularNotConnected.length > 0 && ( -
-

- Popular -

- {popularNotConnected.map((id) => { - const provider = allById.get(id); - return ( -
- - {provider?.name || id} -
- ); - })} -
- )} +
setShowSelectAll(true)} + > + + All providers +
+ + + )} + - {/* Custom + View all */} -
-

Other

-
- - Custom provider - -
- -
- + {/* Connect dialog */} + { + if (!open) setConnectProviderID(null); + }} + > + + + Connect {connectProvider?.name ?? connectProviderID ?? ""} + + {connectProviderID && ( + setConnectProviderID(null)} + /> + )} + + + + {/* Custom provider dialog */} + { + if (!open) setShowCustom(false); + }} + > + + Custom provider + setShowCustom(false)} + /> + + + + {/* Select all providers dialog */} + { + if (!open) setShowSelectAll(false); + }} + > + + All providers + { + setShowSelectAll(false); + setConnectProviderID(id); + }} + onCustom={() => { + setShowSelectAll(false); + setShowCustom(true); + }} + onBack={() => setShowSelectAll(false)} + /> + + + ); } From cf7e0986c95dd7a4def5428e80f1015f1491c8e7 Mon Sep 17 00:00:00 2001 From: akemmanuel Date: Mon, 25 May 2026 14:51:23 -0300 Subject: [PATCH 2/3] ci: switch opencode workflow model to deepseek v4 flash free --- .github/workflows/crocodile.yml | 2 +- .github/workflows/opencode.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/crocodile.yml b/.github/workflows/crocodile.yml index b99f770..18f3000 100644 --- a/.github/workflows/crocodile.yml +++ b/.github/workflows/crocodile.yml @@ -31,7 +31,7 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} with: - model: opencode/minimax-m2.5-free + model: opencode/deepseek-v4-flash-free use_github_token: true prompt: | Review this pull request: diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml index 8148f5e..a39ed1c 100644 --- a/.github/workflows/opencode.yml +++ b/.github/workflows/opencode.yml @@ -30,4 +30,4 @@ jobs: env: OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} with: - model: opencode/minimax-m2.5-free + model: opencode/deepseek-v4-flash-free From 547159b7c6edd9e48f4884f1d691d7aa77c3c22f Mon Sep 17 00:00:00 2001 From: akemmanuel Date: Mon, 25 May 2026 15:02:09 -0300 Subject: [PATCH 3/3] fix: address provider settings review issues --- opencode-bridge.ts | 7 +- src/components/DialogSelectProvider.tsx | 2 +- src/components/SettingsProviders.tsx | 206 ++++++++++++------------ 3 files changed, 110 insertions(+), 105 deletions(-) diff --git a/opencode-bridge.ts b/opencode-bridge.ts index 99d8ac0..06c1b1d 100644 --- a/opencode-bridge.ts +++ b/opencode-bridge.ts @@ -390,16 +390,13 @@ class OpenCodeConnection { async setProviderAuth(providerID, auth) { this._requireClient(); - const res = await this._client.auth.set({ path: { id: providerID }, body: auth }); + const res = await this._client.auth.set({ providerID, auth }); return res.data; } async removeProviderAuth(providerID) { this._requireClient(); - const res = await this._client.auth._client.delete({ - path: { id: providerID }, - url: "/auth/{id}", - }); + const res = await this._client.auth.remove({ providerID }); return res.data; } diff --git a/src/components/DialogSelectProvider.tsx b/src/components/DialogSelectProvider.tsx index a186fd2..1889a59 100644 --- a/src/components/DialogSelectProvider.tsx +++ b/src/components/DialogSelectProvider.tsx @@ -67,7 +67,7 @@ export function DialogSelectProvider({ pop.sort((a, b) => (a.name || a.id).localeCompare(b.name || b.id)); oth.sort((a, b) => (a.name || a.id).localeCompare(b.name || b.id)); return { popular: pop, other: oth }; - }, [providers, connectedIds, lowerSearch]); + }, [providers, lowerSearch]); return (
diff --git a/src/components/SettingsProviders.tsx b/src/components/SettingsProviders.tsx index 423ff16..6aec1e5 100644 --- a/src/components/SettingsProviders.tsx +++ b/src/components/SettingsProviders.tsx @@ -68,7 +68,10 @@ export function SettingsProviders() { const [loading, setLoading] = useState(true); const [disconnecting, setDisconnecting] = useState(null); const [confirmingDisconnect, setConfirmingDisconnect] = useState(null); - const [disconnectError, setDisconnectError] = useState(null); + const [disconnectError, setDisconnectError] = useState<{ + providerID: string; + message: string; + } | null>(null); // Sub-dialog state const [connectProviderID, setConnectProviderID] = useState(null); @@ -95,21 +98,27 @@ export function SettingsProviders() { const isAuthLoading = loading || (!connectProviderID ? false : authMethods[connectProviderID] === undefined); - const refresh = useCallback(async () => { - if (!providersApi) return; - setLoading(true); - const target = { directory: scopedDirectory, workspaceId: activeWorkspaceId }; - const [allProvidersData, providerAuthMethods] = await Promise.all([ - providersApi.listAll(target), - providersApi.getAuthMethods(target), - ]); - setAllProviders(allProvidersData); - setAuthMethods(providerAuthMethods); - setLoading(false); - }, [providersApi, scopedDirectory, activeWorkspaceId]); + const refresh = useCallback( + async (showSpinner = false) => { + if (!providersApi) return; + if (showSpinner) setLoading(true); + try { + const target = { directory: scopedDirectory, workspaceId: activeWorkspaceId }; + const [allProvidersData, providerAuthMethods] = await Promise.all([ + providersApi.listAll(target), + providersApi.getAuthMethods(target), + ]); + setAllProviders(allProvidersData); + setAuthMethods(providerAuthMethods); + } finally { + if (showSpinner) setLoading(false); + } + }, + [providersApi, scopedDirectory, activeWorkspaceId], + ); useEffect(() => { - void refresh(); + void refresh(true); }, [refresh]); const handleDisconnect = async (providerID: string) => { @@ -124,7 +133,10 @@ export function SettingsProviders() { await refresh(); await refreshProviders(); } catch (err) { - setDisconnectError(getErrorMessage(err, "Failed to disconnect")); + setDisconnectError({ + providerID, + message: getErrorMessage(err, "Failed to disconnect"), + }); } finally { setDisconnecting(null); } @@ -147,7 +159,7 @@ export function SettingsProviders() { ); } - if (loading) { + if (loading && !allProviders) { return (
@@ -173,6 +185,13 @@ export function SettingsProviders() { const allById = new Map(providerList.map((p) => [p.id, p])); const connectProvider = connectProviderID ? allById.get(connectProviderID) : null; + const filteredProviders = providerList + .filter( + (p) => + p.id.toLowerCase().includes(lowerSearch) || + (p.name || "").toLowerCase().includes(lowerSearch), + ) + .sort((a, b) => (a.name || a.id).localeCompare(b.name || b.id)); return ( <> @@ -206,106 +225,93 @@ export function SettingsProviders() {
{isSearching ? (
- {providerList - .filter( - (p) => - p.id.toLowerCase().includes(lowerSearch) || - (p.name || "").toLowerCase().includes(lowerSearch), - ) - .sort((a, b) => (a.name || a.id).localeCompare(b.name || b.id)) - .map((provider) => { - const isConnected = connectedSet.has(provider.id); - const isEnv = provider.source === "env"; - const isDisconnecting = disconnecting === provider.id; - const isConfirming = confirmingDisconnect === provider.id; - const showError = disconnectError && !isDisconnecting; - return ( -
-
- - - {provider.name || provider.id} - - {isConnected ? ( - isEnv ? ( - - from env + {filteredProviders.map((provider) => { + const isConnected = connectedSet.has(provider.id); + const isEnv = provider.source === "env"; + const isDisconnecting = disconnecting === provider.id; + const isConfirming = confirmingDisconnect === provider.id; + const showError = disconnectError?.providerID === provider.id && !isDisconnecting; + return ( +
+
+ + + {provider.name || provider.id} + + {isConnected ? ( + isEnv ? ( + from env + ) : isConfirming ? ( +
+ + Disconnect {provider.name || provider.id}? - ) : isConfirming ? ( -
- - Disconnect {provider.name || provider.id}? - - - -
- ) : ( + - ) +
) : ( - )} -
- {showError && ( -
-

{disconnectError}

- -
+ ) + ) : ( + )}
- ); - })} - {providerList.filter( - (p) => - p.id.toLowerCase().includes(lowerSearch) || - (p.name || "").toLowerCase().includes(lowerSearch), - ).length === 0 && ( + {showError && ( +
+

{disconnectError?.message}

+ +
+ )} +
+ ); + })} + {filteredProviders.length === 0 && (
No providers found for "{search}"
@@ -322,7 +328,7 @@ export function SettingsProviders() { const isEnv = provider.source === "env"; const isDisconnecting = disconnecting === provider.id; const isConfirming = confirmingDisconnect === provider.id; - const showError = disconnectError && !isDisconnecting; + const showError = disconnectError?.providerID === provider.id && !isDisconnecting; return (
@@ -389,7 +395,9 @@ export function SettingsProviders() {
{showError && (
-

{disconnectError}

+

+ {disconnectError?.message} +