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
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..1889a59 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 &&
@@ -69,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 (
@@ -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..6aec1e5 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,34 +67,64 @@ 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<{
+ providerID: string;
+ message: string;
+ } | null>(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;
- 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) => {
if (!providersApi) return;
+ setConfirmingDisconnect(null);
+ setDisconnectError(null);
setDisconnecting(providerID);
try {
const target = { directory: scopedDirectory, workspaceId: activeWorkspaceId };
@@ -103,6 +132,11 @@ export function SettingsProviders() {
await providersApi.dispose(target);
await refresh();
await refreshProviders();
+ } catch (err) {
+ setDisconnectError({
+ providerID,
+ message: getErrorMessage(err, "Failed to disconnect"),
+ });
} finally {
setDisconnecting(null);
}
@@ -125,7 +159,7 @@ export function SettingsProviders() {
);
}
- if (loading) {
+ if (loading && !allProviders) {
return (
@@ -144,169 +178,361 @@ 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;
+ 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 (
-
- {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) => (
+
+ ) : (
+ <>
+ {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?.providerID === provider.id && !isDisconnecting;
+ return (
+
+
+
+
+
+
+ {provider.name || provider.id}
+
+
+
+
+ {isEnv ? (
+
+ from env
+
+ ) : isConfirming ? (
+
+
+ Disconnect {provider.name || provider.id}?
+
+ setConfirmingDisconnect(null)}
+ >
+ Cancel
+
+ handleDisconnect(provider.id)}
+ >
+ {isDisconnecting ? (
+
+ ) : (
+
+ )}
+ Disconnect
+
+
+ ) : (
+
setConfirmingDisconnect(provider.id)}
+ >
+ {isDisconnecting ? (
+
+ ) : (
+
+ )}
+ Disconnect
+
+ )}
+
+ {showError && (
+
+
+ {disconnectError?.message}
+
+
setDisconnectError(null)}
+ >
+ dismiss
+
+
+ )}
+
+ );
+ })}
+
+ )}
+
+ {/* Popular providers (not yet connected) */}
+ {popularNotConnected.length > 0 && (
+
+
+ Popular
+
+ {popularNotConnected.map((id) => {
+ const provider = allById.get(id);
+ return (
+
+
+
+ {provider?.name || id}
+
+
setConnectProviderID(id)}>
+
+ Connect
+
+
+ );
+ })}
+
+ )}
- {/* Popular providers (not yet connected) */}
- {popularNotConnected.length > 0 && (
-
-
- Popular
-
- {popularNotConnected.map((id) => {
- const provider = allById.get(id);
- return (
-
-
-
{provider?.name || id}
-
setConnectProviderID(id)}>
+ {/* Custom + View all */}
+
+
+ Other
+
+ setShowCustom(true)}
+ >
+
+
Custom provider
+
Connect
- );
- })}
-
- )}
+ setShowSelectAll(true)}
+ >
+
+
All providers
+
+
+ >
+ )}
+
- {/* Custom + View all */}
-
- Other
-
-
-
Custom provider
-
setShowCustom(true)}>
-
- Connect
-
-
- setShowSelectAll(true)}
- >
- View all providers
-
-
-
+ {/* Connect dialog */}
+
+
+ {/* Custom provider dialog */}
+
+
+ {/* Select all providers dialog */}
+
+ >
);
}