diff --git a/src/components/ProviderSettings.tsx b/src/components/ProviderSettings.tsx index c757752..1ba1f5b 100644 --- a/src/components/ProviderSettings.tsx +++ b/src/components/ProviderSettings.tsx @@ -9,17 +9,36 @@ import { providerSettingsRoute } from "@/routes/settings/providers/$provider"; import type { LanguageModelProvider } from "@/ipc/ipc_types"; import { useLanguageModelProviders } from "@/hooks/useLanguageModelProviders"; -import { GiftIcon, PlusIcon } from "lucide-react"; +import { useCustomLanguageModelProvider } from "@/hooks/useCustomLanguageModelProvider"; +import { GiftIcon, PlusIcon, MoreVertical, Trash2 } from "lucide-react"; import { Skeleton } from "./ui/skeleton"; import { Alert, AlertDescription, AlertTitle } from "./ui/alert"; import { AlertTriangle } from "lucide-react"; import { useState } from "react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; + import { CreateCustomProviderDialog } from "./CreateCustomProviderDialog"; export function ProviderSettingsGrid() { const navigate = useNavigate(); const [isDialogOpen, setIsDialogOpen] = useState(false); + const [providerToDelete, setProviderToDelete] = useState(null); const { data: providers, @@ -29,6 +48,8 @@ export function ProviderSettingsGrid() { refetch, } = useLanguageModelProviders(); + const { deleteProvider, isDeleting } = useCustomLanguageModelProvider(); + const handleProviderClick = (providerId: string) => { navigate({ to: providerSettingsRoute.id, @@ -36,6 +57,14 @@ export function ProviderSettingsGrid() { }); }; + const handleDeleteProvider = async () => { + if (providerToDelete) { + await deleteProvider(providerToDelete); + setProviderToDelete(null); + refetch(); + } + }; + if (isLoading) { return (
@@ -74,13 +103,17 @@ export function ProviderSettingsGrid() {

AI Providers

{providers?.map((provider: LanguageModelProvider) => { + const isCustom = provider.type === "custom"; + return ( handleProviderClick(provider.id)} + className="relative transition-all hover:shadow-md border-border" > - + handleProviderClick(provider.id)} + > {provider.name} {isProviderSetup(provider.id) ? ( @@ -102,6 +135,30 @@ export function ProviderSettingsGrid() { )} + + {isCustom && ( +
e.stopPropagation()} + > + + +
+ +
+
+ + setProviderToDelete(provider.id)} + > + + Delete Provider + + +
+
+ )}
); })} @@ -131,6 +188,30 @@ export function ProviderSettingsGrid() { refetch(); }} /> + + !open && setProviderToDelete(null)} + > + + + Delete Custom Provider + + This will permanently delete this custom provider and all its + associated models. This action cannot be undone. + + + + Cancel + + {isDeleting ? "Deleting..." : "Delete Provider"} + + + +
); } diff --git a/src/hooks/useCustomLanguageModelProvider.ts b/src/hooks/useCustomLanguageModelProvider.ts index 35ddbd5..25d7909 100644 --- a/src/hooks/useCustomLanguageModelProvider.ts +++ b/src/hooks/useCustomLanguageModelProvider.ts @@ -40,15 +40,38 @@ export function useCustomLanguageModelProvider() { }, }); + const deleteProviderMutation = useMutation({ + mutationFn: async (providerId: string): Promise => { + if (!providerId) { + throw new Error("Provider ID is required"); + } + + return ipcClient.deleteCustomLanguageModelProvider(providerId); + }, + onSuccess: () => { + // Invalidate and refetch + queryClient.invalidateQueries({ queryKey: ["languageModelProviders"] }); + }, + onError: (error) => { + showError(error); + }, + }); + const createProvider = async ( params: CreateCustomLanguageModelProviderParams, ): Promise => { return createProviderMutation.mutateAsync(params); }; + const deleteProvider = async (providerId: string): Promise => { + return deleteProviderMutation.mutateAsync(providerId); + }; + return { createProvider, + deleteProvider, isCreating: createProviderMutation.isPending, - error: createProviderMutation.error, + isDeleting: deleteProviderMutation.isPending, + error: createProviderMutation.error || deleteProviderMutation.error, }; } diff --git a/src/ipc/handlers/language_model_handlers.ts b/src/ipc/handlers/language_model_handlers.ts index 8617e49..296d5c5 100644 --- a/src/ipc/handlers/language_model_handlers.ts +++ b/src/ipc/handlers/language_model_handlers.ts @@ -204,6 +204,72 @@ export function registerLanguageModelHandlers() { }, ); + handle( + "delete-custom-language-model-provider", + async ( + event: IpcMainInvokeEvent, + params: { providerId: string }, + ): Promise => { + const { providerId } = params; + + // Validation + if (!providerId) { + throw new Error("Provider ID is required"); + } + + logger.info( + `Handling delete-custom-language-model-provider for providerId: ${providerId}`, + ); + + // Check if the provider exists before attempting deletion + const existingProvider = await db + .select({ id: languageModelProvidersSchema.id }) + .from(languageModelProvidersSchema) + .where(eq(languageModelProvidersSchema.id, providerId)) + .get(); + + if (!existingProvider) { + // If the provider doesn't exist, maybe it was already deleted. Log and return. + logger.warn( + `Provider with ID "${providerId}" not found. It might have been deleted already.`, + ); + // Optionally, throw new Error(`Provider with ID "${providerId}" not found`); + // Deciding to return gracefully instead of throwing an error if not found. + return; + } + + // Use a transaction to ensure atomicity + await db.transaction(async (tx) => { + // 1. Delete associated models + const deleteModelsResult = await tx + .delete(languageModelsSchema) + .where(eq(languageModelsSchema.provider_id, providerId)) + .run(); + logger.info( + `Deleted ${deleteModelsResult.changes} model(s) associated with provider ${providerId}`, + ); + + // 2. Delete the provider + const deleteProviderResult = await tx + .delete(languageModelProvidersSchema) + .where(eq(languageModelProvidersSchema.id, providerId)) + .run(); + + if (deleteProviderResult.changes === 0) { + // This case should ideally not happen if existingProvider check passed, + // but adding safety check within transaction. + logger.error( + `Failed to delete provider with ID "${providerId}" during transaction, although it was found initially. Rolling back.`, + ); + throw new Error( + `Failed to delete provider with ID "${providerId}" which should have existed.`, + ); + } + logger.info(`Successfully deleted provider with ID "${providerId}".`); + }); + }, + ); + handle( "get-language-models", async ( diff --git a/src/ipc/ipc_client.ts b/src/ipc/ipc_client.ts index 7675575..24f5192 100644 --- a/src/ipc/ipc_client.ts +++ b/src/ipc/ipc_client.ts @@ -774,6 +774,12 @@ export class IpcClient { return this.ipcRenderer.invoke("delete-custom-model", params); } + async deleteCustomLanguageModelProvider(providerId: string): Promise { + return this.ipcRenderer.invoke("delete-custom-language-model-provider", { + providerId, + }); + } + // --- End window control methods --- // --- Language Model Operations --- diff --git a/src/preload.ts b/src/preload.ts index 75a6b71..82ac7c1 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -8,6 +8,7 @@ const validInvokeChannels = [ "get-language-models", "create-custom-language-model", "get-language-model-providers", + "delete-custom-language-model-provider", "create-custom-language-model-provider", "delete-custom-language-model", "delete-custom-model",