diff --git a/apps/web/components/settings/agent-profile-delete-dialog.tsx b/apps/web/components/settings/agent-profile-delete-dialog.tsx index d05c332846..82682c06b7 100644 --- a/apps/web/components/settings/agent-profile-delete-dialog.tsx +++ b/apps/web/components/settings/agent-profile-delete-dialog.tsx @@ -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 ( + + + + Delete agent profile? + + This will permanently delete this profile. This action cannot be undone. + + + + Cancel + + Delete + + + + + ); +} + +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) ?? []; diff --git a/apps/web/components/settings/agent-profile-page.tsx b/apps/web/components/settings/agent-profile-page.tsx index 551dd998c9..a26e4346c6 100644 --- a/apps/web/components/settings/agent-profile-page.tsx +++ b/apps/web/components/settings/agent-profile-page.tsx @@ -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["toast"], ) { + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [conflictSessions, setConflictSessions] = useState(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 (
@@ -376,9 +400,17 @@ function ProfileEditor({ } /> - + - { + if (!open) setShowDeleteConfirm(false); + }} + onConfirm={handleDeleteProfile} + /> + + { if (!open) setConflictSessions(null); diff --git a/apps/web/e2e/tests/settings/agent-profile-delete.spec.ts b/apps/web/e2e/tests/settings/agent-profile-delete.spec.ts index 4186820e7e..5e1551b8f6 100644 --- a/apps/web/e2e/tests/settings/agent-profile-delete.spec.ts +++ b/apps/web/e2e/tests/settings/agent-profile-delete.spec.ts @@ -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 });