fix(web): add confirmation dialog before deleting agent profile (#596)

This commit is contained in:
Carlos Florêncio
2026-04-15 22:16:34 +01:00
committed by GitHub
parent a2d98f5d3e
commit 1f4451b192
3 changed files with 113 additions and 28 deletions
@@ -12,17 +12,51 @@ import {
} from "@kandev/ui/alert-dialog";
import type { ActiveSessionInfo } from "@/lib/types/agent-profile-errors";
type AgentProfileDeleteDialogProps = {
type AgentProfileDeleteConfirmDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: () => void;
};
export function AgentProfileDeleteConfirmDialog({
open,
onOpenChange,
onConfirm,
}: AgentProfileDeleteConfirmDialogProps) {
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete agent profile?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete this profile. This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="cursor-pointer">Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={onConfirm}
className="cursor-pointer bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
type AgentProfileDeleteConflictDialogProps = {
activeSessions: ActiveSessionInfo[] | null;
onOpenChange: (open: boolean) => void;
onConfirm: () => void;
};
export function AgentProfileDeleteDialog({
export function AgentProfileDeleteConflictDialog({
activeSessions,
onOpenChange,
onConfirm,
}: AgentProfileDeleteDialogProps) {
}: AgentProfileDeleteConflictDialogProps) {
const tasks = activeSessions?.filter((s) => !s.is_ephemeral) ?? [];
const quickChats = activeSessions?.filter((s) => s.is_ephemeral) ?? [];
@@ -13,7 +13,10 @@ import { UnsavedChangesBadge, UnsavedSaveButton } from "@/components/settings/un
import { ProfileFormFields } from "@/components/settings/profile-form-fields";
import { deleteAgentProfileAction, updateAgentProfileAction } from "@/app/actions/agents";
import type { ActiveSessionInfo } from "@/lib/types/agent-profile-errors";
import { AgentProfileDeleteDialog } from "@/components/settings/agent-profile-delete-dialog";
import {
AgentProfileDeleteConfirmDialog,
AgentProfileDeleteConflictDialog,
} from "@/components/settings/agent-profile-delete-dialog";
import type {
Agent,
AgentProfile,
@@ -266,6 +269,7 @@ function useProfileDelete(
syncAgentsToStore: (agents: Agent[]) => void,
toast: ReturnType<typeof useToast>["toast"],
) {
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [conflictSessions, setConflictSessions] = useState<ActiveSessionInfo[] | null>(null);
const removeProfileFromStore = () => {
@@ -281,7 +285,12 @@ function useProfileDelete(
window.location.assign("/settings/agents");
};
const requestDelete = () => {
setShowDeleteConfirm(true);
};
const handleDeleteProfile = async () => {
setShowDeleteConfirm(false);
const result = await deleteAgentProfileAction(draft.id);
if (result.status === "ok") {
removeProfileFromStore();
@@ -302,7 +311,15 @@ function useProfileDelete(
}
};
return { handleDeleteProfile, conflictSessions, setConflictSessions, handleForceDelete };
return {
requestDelete,
showDeleteConfirm,
setShowDeleteConfirm,
handleDeleteProfile,
conflictSessions,
setConflictSessions,
handleForceDelete,
};
}
function ProfileEditor({
@@ -328,8 +345,15 @@ function ProfileEditor({
syncAgentsToStore,
toast,
});
const { handleDeleteProfile, conflictSessions, setConflictSessions, handleForceDelete } =
useProfileDelete(agent, draft, settingsAgents, syncAgentsToStore, toast);
const {
requestDelete,
showDeleteConfirm,
setShowDeleteConfirm,
handleDeleteProfile,
conflictSessions,
setConflictSessions,
handleForceDelete,
} = useProfileDelete(agent, draft, settingsAgents, syncAgentsToStore, toast);
return (
<div className="space-y-8">
@@ -376,9 +400,17 @@ function ProfileEditor({
}
/>
<DeleteProfileCard onDelete={handleDeleteProfile} />
<DeleteProfileCard onDelete={requestDelete} />
<AgentProfileDeleteDialog
<AgentProfileDeleteConfirmDialog
open={showDeleteConfirm}
onOpenChange={(open) => {
if (!open) setShowDeleteConfirm(false);
}}
onConfirm={handleDeleteProfile}
/>
<AgentProfileDeleteConflictDialog
activeSessions={conflictSessions}
onOpenChange={(open) => {
if (!open) setConflictSessions(null);
@@ -1,7 +1,7 @@
import { test, expect } from "../../fixtures/test-base";
test.describe("Agent profile deletion", () => {
test("deleting profile with no active sessions succeeds immediately", async ({
test("deleting profile with no active sessions shows confirm dialog then succeeds", async ({
testPage,
apiClient,
}) => {
@@ -17,7 +17,6 @@ test.describe("Agent profile deletion", () => {
// Navigate to profile settings page
await testPage.goto(`/settings/agents/${agent.name}/profiles/${profile.id}`);
// Wait for the profile page to load — use the heading which includes the profile name
// Wait for the delete card to load (the card title is "Delete profile")
await expect(testPage.getByText("Delete profile", { exact: true })).toBeVisible({
timeout: 15_000,
@@ -26,11 +25,19 @@ test.describe("Agent profile deletion", () => {
// Click the delete button inside the delete card
await testPage.getByRole("button", { name: "Delete", exact: true }).click();
// Should redirect to agents settings page (no dialog since no active sessions)
// Confirmation dialog should appear
const dialog = testPage.getByRole("alertdialog");
await expect(dialog).toBeVisible({ timeout: 10_000 });
await expect(dialog.getByText("This action cannot be undone")).toBeVisible();
// Confirm the deletion
await dialog.getByRole("button", { name: "Delete", exact: true }).click();
// Should redirect to agents settings page
await expect(testPage).toHaveURL(/\/settings\/agents$/, { timeout: 15_000 });
});
test("deleting profile with active task shows conflict dialog and allows cancel", async ({
test("deleting profile with active task shows confirm then conflict dialog and allows cancel", async ({
testPage,
apiClient,
seedData,
@@ -60,7 +67,6 @@ test.describe("Agent profile deletion", () => {
// Navigate to profile settings page
await testPage.goto(`/settings/agents/${agent.name}/profiles/${profile.id}`);
// Wait for the profile page to load
// Wait for the delete card to load (the card title is "Delete profile")
await expect(testPage.getByText("Delete profile", { exact: true })).toBeVisible({
timeout: 15_000,
@@ -69,23 +75,30 @@ test.describe("Agent profile deletion", () => {
// Click the delete button
await testPage.getByRole("button", { name: "Delete", exact: true }).click();
// The conflict dialog should appear
const dialog = testPage.getByRole("alertdialog");
await expect(dialog).toBeVisible({ timeout: 10_000 });
await expect(dialog.getByText("Active Task For Profile")).toBeVisible();
await expect(dialog.getByText("This profile is currently in use")).toBeVisible();
// Initial confirmation dialog should appear
const confirmDialog = testPage.getByRole("alertdialog");
await expect(confirmDialog).toBeVisible({ timeout: 10_000 });
// Confirm the initial deletion
await confirmDialog.getByRole("button", { name: "Delete", exact: true }).click();
// The conflict dialog should appear with active session info
const conflictDialog = testPage.getByRole("alertdialog");
await expect(conflictDialog).toBeVisible({ timeout: 10_000 });
await expect(conflictDialog.getByText("Active Task For Profile")).toBeVisible();
await expect(conflictDialog.getByText("This profile is currently in use")).toBeVisible();
// Cancel the deletion
await dialog.getByRole("button", { name: "Cancel" }).click();
await conflictDialog.getByRole("button", { name: "Cancel" }).click();
// Dialog should close and we should still be on the profile page
await expect(dialog).not.toBeVisible();
await expect(conflictDialog).not.toBeVisible();
await expect(testPage).toHaveURL(
new RegExp(`/settings/agents/${agent.name}/profiles/${profile.id}`),
);
});
test("force-deleting profile with active task succeeds after confirmation", async ({
test("force-deleting profile with active task succeeds after both confirmations", async ({
testPage,
apiClient,
seedData,
@@ -110,7 +123,6 @@ test.describe("Agent profile deletion", () => {
// Navigate to profile settings page
await testPage.goto(`/settings/agents/${agent.name}/profiles/${profile.id}`);
// Wait for the profile page to load
// Wait for the delete card to load (the card title is "Delete profile")
await expect(testPage.getByText("Delete profile", { exact: true })).toBeVisible({
timeout: 15_000,
@@ -119,13 +131,20 @@ test.describe("Agent profile deletion", () => {
// Click the delete button
await testPage.getByRole("button", { name: "Delete", exact: true }).click();
// Conflict dialog should appear
const dialog = testPage.getByRole("alertdialog");
await expect(dialog).toBeVisible({ timeout: 10_000 });
await expect(dialog.getByText("Task For Force Delete")).toBeVisible();
// Initial confirmation dialog should appear
const confirmDialog = testPage.getByRole("alertdialog");
await expect(confirmDialog).toBeVisible({ timeout: 10_000 });
// Confirm the initial deletion
await confirmDialog.getByRole("button", { name: "Delete", exact: true }).click();
// Conflict dialog should appear with active session info
const conflictDialog = testPage.getByRole("alertdialog");
await expect(conflictDialog).toBeVisible({ timeout: 10_000 });
await expect(conflictDialog.getByText("Task For Force Delete")).toBeVisible();
// Confirm force deletion
await dialog.getByRole("button", { name: "Delete Anyway" }).click();
await conflictDialog.getByRole("button", { name: "Delete Anyway" }).click();
// Should redirect to agents settings page
await expect(testPage).toHaveURL(/\/settings\/agents$/, { timeout: 15_000 });