feat: add edit functionality for custom AI providers (#1232) (#1171) (#1250)

## Summary
Adds the ability to edit existing custom AI providers through the
settings UI.

## Changes Made
- **UI Changes:**
  - Added "Edit Provider" button to custom provider dropdown menu
  - Modified `CreateCustomProviderDialog` to support edit mode


- **Backend Changes:**
- Implemented `editCustomLanguageModelProvider` handler in
`language_model_handlers.ts`
  - Added corresponding IPC client method
- Database transaction ensures atomicity when updating provider and
associated models


- **Testing:**
  - Added comprehensive e2e test covering edit functionality
  - Tests verify form pre-population, field updates, and UI persistence
  
  


https://github.com/user-attachments/assets/e8c8600e-4fb7-4816-be95-993ede1224d4



## Closes
Fixes #1232 and #1171
    
<!-- This is an auto-generated description by cubic. -->
---

## Summary by cubic
Adds edit support for custom language model providers in Settings. Users
can update provider ID, name, API base URL, and API key env var, with
safe backend updates that also retarget associated models if the ID
changes.

- New Features
  - Added “Edit Provider” option in the custom provider menu.
- Dialog supports edit mode with pre-filled fields, unified loading
state, and update button text.
- New IPC handler to edit providers with validation and a transaction;
updates linked models when IDs change.
- IPC client and preload channel updated; React hook exposes
editProvider mutation with cache invalidation.
  - Added e2e test covering the full edit flow.

<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Adeniji Adekunle James
2025-09-17 06:58:46 +01:00
committed by GitHub
parent decd05e764
commit 8c3fdb0ad0
7 changed files with 229 additions and 25 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import {
Dialog,
DialogContent,
@@ -11,38 +11,73 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Loader2 } from "lucide-react";
import { useCustomLanguageModelProvider } from "@/hooks/useCustomLanguageModelProvider";
import type { LanguageModelProvider } from "@/ipc/ipc_types";
interface CreateCustomProviderDialogProps {
isOpen: boolean;
onClose: () => void;
onSuccess: () => void;
editingProvider?: LanguageModelProvider | null;
}
export function CreateCustomProviderDialog({
isOpen,
onClose,
onSuccess,
editingProvider = null,
}: CreateCustomProviderDialogProps) {
const [id, setId] = useState("");
const [name, setName] = useState("");
const [apiBaseUrl, setApiBaseUrl] = useState("");
const [envVarName, setEnvVarName] = useState("");
const [errorMessage, setErrorMessage] = useState("");
const isEditMode = Boolean(editingProvider);
const { createProvider, isCreating, error } =
const { createProvider, editProvider, isCreating, isEditing, error } =
useCustomLanguageModelProvider();
// Load provider data when editing
useEffect(() => {
if (editingProvider && isOpen) {
const cleanId = editingProvider.id?.startsWith("custom::")
? editingProvider.id.replace("custom::", "")
: editingProvider.id || "";
setId(cleanId);
setName(editingProvider.name || "");
setApiBaseUrl(editingProvider.apiBaseUrl || "");
setEnvVarName(editingProvider.envVarName || "");
} else if (!isOpen) {
// Reset form when dialog closes
setId("");
setName("");
setApiBaseUrl("");
setEnvVarName("");
setErrorMessage("");
}
}, [editingProvider, isOpen]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setErrorMessage("");
try {
await createProvider({
id: id.trim(),
name: name.trim(),
apiBaseUrl: apiBaseUrl.trim(),
envVarName: envVarName.trim() || undefined,
});
if (isEditMode && editingProvider) {
const cleanId = editingProvider.id?.startsWith("custom::")
? editingProvider.id.replace("custom::", "")
: editingProvider.id || "";
await editProvider({
id: cleanId,
name: name.trim(),
apiBaseUrl: apiBaseUrl.trim(),
envVarName: envVarName.trim() || undefined,
});
} else {
await createProvider({
id: id.trim(),
name: name.trim(),
apiBaseUrl: apiBaseUrl.trim(),
envVarName: envVarName.trim() || undefined,
});
}
// Reset form
setId("");
@@ -55,25 +90,30 @@ export function CreateCustomProviderDialog({
setErrorMessage(
error instanceof Error
? error.message
: "Failed to create custom provider",
: `Failed to ${isEditMode ? "edit" : "create"} custom provider`,
);
}
};
const handleClose = () => {
if (!isCreating) {
if (!isCreating && !isEditing) {
setErrorMessage("");
onClose();
}
};
const isLoading = isCreating || isEditing;
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Add Custom Provider</DialogTitle>
<DialogTitle>
{isEditMode ? "Edit Custom Provider" : "Add Custom Provider"}
</DialogTitle>
<DialogDescription>
Connect to a custom language model provider API
{isEditMode
? "Update your custom language model provider configuration."
: "Connect to a custom language model provider API."}
</DialogDescription>
</DialogHeader>
@@ -86,7 +126,7 @@ export function CreateCustomProviderDialog({
onChange={(e) => setId(e.target.value)}
placeholder="E.g., my-provider"
required
disabled={isCreating}
disabled={isLoading || isEditMode}
/>
<p className="text-xs text-muted-foreground">
A unique identifier for this provider (no spaces).
@@ -101,7 +141,7 @@ export function CreateCustomProviderDialog({
onChange={(e) => setName(e.target.value)}
placeholder="E.g., My Provider"
required
disabled={isCreating}
disabled={isLoading}
/>
<p className="text-xs text-muted-foreground">
The name that will be displayed in the UI.
@@ -116,7 +156,7 @@ export function CreateCustomProviderDialog({
onChange={(e) => setApiBaseUrl(e.target.value)}
placeholder="E.g., https://api.example.com/v1"
required
disabled={isCreating}
disabled={isLoading}
/>
<p className="text-xs text-muted-foreground">
The base URL for the API endpoint.
@@ -130,7 +170,7 @@ export function CreateCustomProviderDialog({
value={envVarName}
onChange={(e) => setEnvVarName(e.target.value)}
placeholder="E.g., MY_PROVIDER_API_KEY"
disabled={isCreating}
disabled={isLoading}
/>
<p className="text-xs text-muted-foreground">
Environment variable name for the API key.
@@ -151,13 +191,19 @@ export function CreateCustomProviderDialog({
type="button"
variant="outline"
onClick={handleClose}
disabled={isCreating}
disabled={isLoading}
>
Cancel
</Button>
<Button type="submit" disabled={isCreating}>
{isCreating && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{isCreating ? "Adding..." : "Add Provider"}
<Button type="submit" disabled={isLoading}>
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{isLoading
? isEditMode
? "Updating..."
: "Adding..."
: isEditMode
? "Update Provider"
: "Add Provider"}
</Button>
</div>
</form>