feat: add delete session functionality with confirmation dialog

- Introduced a new DeleteSessionDialog component for confirming session deletions.
- Integrated the delete session dialog into the SessionManager component, allowing users to delete sessions with a confirmation prompt.
- Updated the UI to handle session deletion more intuitively, enhancing user experience.
- Refactored existing delete confirmation logic to utilize the new DeleteConfirmDialog component for consistency across the application.
This commit is contained in:
Cody Seibert
2025-12-12 19:41:52 -05:00
parent 437063630c
commit fe9b26c49e
11 changed files with 289 additions and 334 deletions

View File

@@ -0,0 +1,52 @@
import { MessageSquare } from "lucide-react";
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog";
import type { SessionListItem } from "@/types/electron";
interface DeleteSessionDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
session: SessionListItem | null;
onConfirm: (sessionId: string) => void;
}
export function DeleteSessionDialog({
open,
onOpenChange,
session,
onConfirm,
}: DeleteSessionDialogProps) {
const handleConfirm = () => {
if (session) {
onConfirm(session.id);
}
};
return (
<DeleteConfirmDialog
open={open}
onOpenChange={onOpenChange}
onConfirm={handleConfirm}
title="Delete Session"
description="Are you sure you want to delete this session? This action cannot be undone."
confirmText="Delete Session"
testId="delete-session-dialog"
confirmTestId="confirm-delete-session"
>
{session && (
<div className="flex items-center gap-3 p-4 rounded-lg bg-sidebar-accent/10 border border-sidebar-border">
<div className="w-10 h-10 rounded-lg bg-sidebar-accent/20 border border-sidebar-border flex items-center justify-center shrink-0">
<MessageSquare className="w-5 h-5 text-brand-500" />
</div>
<div className="min-w-0">
<p className="font-medium text-foreground truncate">
{session.name}
</p>
<p className="text-xs text-muted-foreground">
{session.messageCount} messages
</p>
</div>
</div>
)}
</DeleteConfirmDialog>
);
}

View File

@@ -26,6 +26,7 @@ import { cn } from "@/lib/utils";
import type { SessionListItem } from "@/types/electron";
import { useKeyboardShortcutsConfig } from "@/hooks/use-keyboard-shortcuts";
import { getElectronAPI } from "@/lib/electron";
import { DeleteSessionDialog } from "@/components/delete-session-dialog";
// Random session name generator
const adjectives = [
@@ -113,6 +114,8 @@ export function SessionManager({
const [runningSessions, setRunningSessions] = useState<Set<string>>(
new Set()
);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [sessionToDelete, setSessionToDelete] = useState<SessionListItem | null>(null);
// Check running state for all sessions
const checkRunningSessions = async (sessionList: SessionListItem[]) => {
@@ -286,11 +289,16 @@ export function SessionManager({
}
};
// Delete session
const handleDeleteSession = async (sessionId: string) => {
// Open delete session dialog
const handleDeleteSession = (session: SessionListItem) => {
setSessionToDelete(session);
setIsDeleteDialogOpen(true);
};
// Confirm delete session
const confirmDeleteSession = async (sessionId: string) => {
const api = getElectronAPI();
if (!api?.sessions) return;
if (!confirm("Are you sure you want to delete this session?")) return;
const result = await api.sessions.delete(sessionId);
if (result.success) {
@@ -303,6 +311,7 @@ export function SessionManager({
}
}
}
setSessionToDelete(null);
};
const activeSessions = sessions.filter((s) => !s.isArchived);
@@ -315,20 +324,24 @@ export function SessionManager({
<CardHeader className="pb-3">
<div className="flex items-center justify-between mb-4">
<CardTitle>Agent Sessions</CardTitle>
{activeTab === "active" && (
<HotkeyButton
variant="default"
size="sm"
onClick={handleQuickCreateSession}
hotkey={shortcuts.newSession}
hotkeyActive={false}
data-testid="new-session-button"
title={`New Session (${shortcuts.newSession})`}
>
<Plus className="w-4 h-4 mr-1" />
New
</HotkeyButton>
)}
<HotkeyButton
variant="default"
size="sm"
onClick={() => {
// Switch to active tab if on archived tab
if (activeTab === "archived") {
setActiveTab("active");
}
handleQuickCreateSession();
}}
hotkey={shortcuts.newSession}
hotkeyActive={false}
data-testid="new-session-button"
title={`New Session (${shortcuts.newSession})`}
>
<Plus className="w-4 h-4 mr-1" />
New
</HotkeyButton>
</div>
<Tabs
@@ -525,8 +538,9 @@ export function SessionManager({
<Button
size="sm"
variant="ghost"
onClick={() => handleDeleteSession(session.id)}
onClick={() => handleDeleteSession(session)}
className="h-7 w-7 p-0 text-destructive"
data-testid={`delete-session-${session.id}`}
>
<Trash2 className="w-3 h-3" />
</Button>
@@ -552,6 +566,14 @@ export function SessionManager({
</div>
)}
</CardContent>
{/* Delete Session Confirmation Dialog */}
<DeleteSessionDialog
open={isDeleteDialogOpen}
onOpenChange={setIsDeleteDialogOpen}
session={sessionToDelete}
onConfirm={confirmDeleteSession}
/>
</Card>
);
}

View File

@@ -0,0 +1,88 @@
import { Trash2 } from "lucide-react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { HotkeyButton } from "@/components/ui/hotkey-button";
import type { ReactNode } from "react";
interface DeleteConfirmDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: () => void;
title: string;
description: string;
/** Optional content to show between description and buttons (e.g., item preview card) */
children?: ReactNode;
/** Text for the confirm button. Defaults to "Delete" */
confirmText?: string;
/** Test ID for the dialog */
testId?: string;
/** Test ID for the confirm button */
confirmTestId?: string;
}
export function DeleteConfirmDialog({
open,
onOpenChange,
onConfirm,
title,
description,
children,
confirmText = "Delete",
testId = "delete-confirm-dialog",
confirmTestId = "confirm-delete-button",
}: DeleteConfirmDialogProps) {
const handleConfirm = () => {
onConfirm();
onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="bg-popover border-border max-w-md"
data-testid={testId}
>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Trash2 className="w-5 h-5 text-destructive" />
{title}
</DialogTitle>
<DialogDescription className="text-muted-foreground">
{description}
</DialogDescription>
</DialogHeader>
{children}
<DialogFooter className="gap-2 sm:gap-2 pt-4">
<Button
variant="ghost"
onClick={() => onOpenChange(false)}
className="px-4"
data-testid="cancel-delete-button"
>
Cancel
</Button>
<HotkeyButton
variant="destructive"
onClick={handleConfirm}
data-testid={confirmTestId}
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkeyActive={open}
className="px-4"
>
<Trash2 className="w-4 h-4 mr-2" />
{confirmText}
</HotkeyButton>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -21,6 +21,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog";
import {
DropdownMenu,
DropdownMenuContent,
@@ -195,14 +196,9 @@ export const KanbanCard = memo(function KanbanCard({
};
const handleConfirmDelete = () => {
setIsDeleteDialogOpen(false);
onDelete();
};
const handleCancelDelete = () => {
setIsDeleteDialogOpen(false);
};
// Dragging logic:
// - Backlog items can always be dragged
// - skipTests items can be dragged even when in_progress or verified (unless currently running)
@@ -805,35 +801,15 @@ export const KanbanCard = memo(function KanbanCard({
</CardContent>
{/* Delete Confirmation Dialog */}
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<DialogContent data-testid="delete-confirmation-dialog">
<DialogHeader>
<DialogTitle>Delete Feature</DialogTitle>
<DialogDescription>
Are you sure you want to delete this feature? This action cannot
be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter className="mt-6">
<Button
variant="ghost"
onClick={handleCancelDelete}
data-testid="cancel-delete-button"
>
Cancel
</Button>
<HotkeyButton
variant="destructive"
onClick={handleConfirmDelete}
data-testid="confirm-delete-button"
hotkey={{ key: "Enter", cmdCtrl: true }}
hotkeyActive={isDeleteDialogOpen}
>
Delete
</HotkeyButton>
</DialogFooter>
</DialogContent>
</Dialog>
<DeleteConfirmDialog
open={isDeleteDialogOpen}
onOpenChange={setIsDeleteDialogOpen}
onConfirm={handleConfirmDelete}
title="Delete Feature"
description="Are you sure you want to delete this feature? This action cannot be undone."
testId="delete-confirmation-dialog"
confirmTestId="confirm-delete-button"
/>
{/* Summary Modal */}
<Dialog open={isSummaryDialogOpen} onOpenChange={setIsSummaryDialogOpen}>

View File

@@ -1,13 +1,5 @@
import { Trash2, Folder } from "lucide-react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Folder, Trash2 } from "lucide-react";
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog";
import type { Project } from "@/lib/electron";
interface DeleteProjectDialogProps {
@@ -26,24 +18,22 @@ export function DeleteProjectDialog({
const handleConfirm = () => {
if (project) {
onConfirm(project.id);
onOpenChange(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="bg-popover border-border max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Trash2 className="w-5 h-5 text-destructive" />
Delete Project
</DialogTitle>
<DialogDescription className="text-muted-foreground">
Are you sure you want to move this project to Trash?
</DialogDescription>
</DialogHeader>
{project && (
<DeleteConfirmDialog
open={open}
onOpenChange={onOpenChange}
onConfirm={handleConfirm}
title="Delete Project"
description="Are you sure you want to move this project to Trash?"
confirmText="Move to Trash"
testId="delete-project-dialog"
confirmTestId="confirm-delete-project"
>
{project && (
<>
<div className="flex items-center gap-3 p-4 rounded-lg bg-sidebar-accent/10 border border-sidebar-border">
<div className="w-10 h-10 rounded-lg bg-sidebar-accent/20 border border-sidebar-border flex items-center justify-center shrink-0">
<Folder className="w-5 h-5 text-brand-500" />
@@ -57,27 +47,13 @@ export function DeleteProjectDialog({
</p>
</div>
</div>
)}
<p className="text-sm text-muted-foreground">
The folder will remain on disk until you permanently delete it from
Trash.
</p>
<DialogFooter className="gap-2 sm:gap-0">
<Button variant="ghost" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleConfirm}
data-testid="confirm-delete-project"
>
<Trash2 className="w-4 h-4 mr-2" />
Move to Trash
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<p className="text-sm text-muted-foreground">
The folder will remain on disk until you permanently delete it from
Trash.
</p>
</>
)}
</DeleteConfirmDialog>
);
}

View File

@@ -147,7 +147,7 @@ export const DEFAULT_KEYBOARD_SHORTCUTS: KeyboardShortcuts = {
// Note: Some shortcuts share the same key (e.g., "N" for addFeature, newSession, addProfile)
// This is intentional as they are context-specific and only active in their respective views
addFeature: "N", // Only active in board view
addContextFile: "F", // Only active in context view
addContextFile: "N", // Only active in context view
startNext: "G", // Only active in board view
newSession: "N", // Only active in agent view
openProject: "O", // Global shortcut
@@ -1136,6 +1136,20 @@ export const useAppStore = create<AppState & AppActions>()(
}),
{
name: "automaker-storage",
version: 1, // Increment when making breaking changes to persisted state
migrate: (persistedState: unknown, version: number) => {
const state = persistedState as Partial<AppState>;
// Migration from version 0 (no version) to version 1:
// - Change addContextFile shortcut from "F" to "N"
if (version === 0) {
if (state.keyboardShortcuts?.addContextFile === "F") {
state.keyboardShortcuts.addContextFile = "N";
}
}
return state as AppState;
},
partialize: (state) => ({
// Project management
projects: state.projects,