diff --git a/crates/goose-server/src/routes/session.rs b/crates/goose-server/src/routes/session.rs index 95f4ee1dbf32..432bbc398104 100644 --- a/crates/goose-server/src/routes/session.rs +++ b/crates/goose-server/src/routes/session.rs @@ -7,7 +7,7 @@ use crate::state::AppState; use axum::{ extract::{Path, State}, http::{HeaderMap, StatusCode}, - routing::{get, put}, + routing::{delete, get, put}, Json, Router, }; use goose::conversation::message::Message; @@ -310,11 +310,54 @@ async fn update_session_metadata( Ok(StatusCode::OK) } +#[utoipa::path( + delete, + path = "/sessions/{session_id}/delete", + params( + ("session_id" = String, Path, description = "Unique identifier for the session") + ), + responses( + (status = 200, description = "Session deleted successfully"), + (status = 401, description = "Unauthorized - Invalid or missing API key"), + (status = 404, description = "Session not found"), + (status = 500, description = "Internal server error") + ), + security( + ("api_key" = []) + ), + tag = "Session Management" +)] +// Delete a session +async fn delete_session( + State(state): State>, + headers: HeaderMap, + Path(session_id): Path, +) -> Result { + verify_secret_key(&headers, &state)?; + + // Get the session path + let session_path = match session::get_path(session::Identifier::Name(session_id.clone())) { + Ok(path) => path, + Err(_) => return Err(StatusCode::BAD_REQUEST), + }; + + // Check if session file exists + if !session_path.exists() { + return Err(StatusCode::NOT_FOUND); + } + + // Delete the session file + std::fs::remove_file(&session_path).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(StatusCode::OK) +} + // Configure routes for this module pub fn routes(state: Arc) -> Router { Router::new() .route("/sessions", get(list_sessions)) .route("/sessions/{session_id}", get(get_session_history)) + .route("/sessions/{session_id}/delete", delete(delete_session)) .route("/sessions/insights", get(get_session_insights)) .route( "/sessions/{session_id}/metadata", diff --git a/ui/desktop/src/components/sessions/SessionListView.tsx b/ui/desktop/src/components/sessions/SessionListView.tsx index a108903d9549..935a47e5757d 100644 --- a/ui/desktop/src/components/sessions/SessionListView.tsx +++ b/ui/desktop/src/components/sessions/SessionListView.tsx @@ -1,6 +1,14 @@ import React, { useEffect, useState, useRef, useCallback, useMemo, startTransition } from 'react'; -import { MessageSquareText, Target, AlertCircle, Calendar, Folder, Edit2 } from 'lucide-react'; -import { fetchSessions, updateSessionMetadata, type Session } from '../../sessions'; +import { + MessageSquareText, + Target, + AlertCircle, + Calendar, + Folder, + Edit2, + Trash2, +} from 'lucide-react'; +import { fetchSessions, updateSessionMetadata, deleteSession, type Session } from '../../sessions'; import { Card } from '../ui/card'; import { Button } from '../ui/button'; import { ScrollArea } from '../ui/scroll-area'; @@ -12,6 +20,7 @@ import { MainPanelLayout } from '../Layout/MainPanelLayout'; import { groupSessionsByDate, type DateGroup } from '../../utils/dateUtils'; import { Skeleton } from '../ui/skeleton'; import { toast } from 'react-toastify'; +import { ConfirmationModal } from '../ui/ConfirmationModal'; interface EditSessionModalProps { session: Session | null; @@ -177,6 +186,10 @@ const SessionListView: React.FC = React.memo( const [showEditModal, setShowEditModal] = useState(false); const [editingSession, setEditingSession] = useState(null); + // Delete confirmation modal state + const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false); + const [sessionToDelete, setSessionToDelete] = useState(null); + // Search state for debouncing const [searchTerm, setSearchTerm] = useState(''); const [caseSensitive, setCaseSensitive] = useState(false); @@ -355,12 +368,43 @@ const SessionListView: React.FC = React.memo( setShowEditModal(true); }, []); + const handleDeleteSession = useCallback((session: Session) => { + setSessionToDelete(session); + setShowDeleteConfirmation(true); + }, []); + + const handleConfirmDelete = useCallback(async () => { + if (!sessionToDelete) return; + + setShowDeleteConfirmation(false); + const sessionToDeleteId = sessionToDelete.id; + const sessionName = sessionToDelete.metadata.description || sessionToDelete.id; + setSessionToDelete(null); + + try { + await deleteSession(sessionToDeleteId); + toast.success('Session deleted successfully'); + loadSessions(); + } catch (error) { + console.error('Error deleting session:', error); + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + toast.error(`Failed to delete session "${sessionName}": ${errorMessage}`); + } + }, [sessionToDelete, loadSessions]); + + const handleCancelDelete = useCallback(() => { + setShowDeleteConfirmation(false); + setSessionToDelete(null); + }, []); + const SessionItem = React.memo(function SessionItem({ session, onEditClick, + onDeleteClick, }: { session: Session; onEditClick: (session: Session) => void; + onDeleteClick: (session: Session) => void; }) { const handleEditClick = useCallback( (e: React.MouseEvent) => { @@ -370,6 +414,14 @@ const SessionListView: React.FC = React.memo( [onEditClick, session] ); + const handleDeleteClick = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); // Prevent card click + onDeleteClick(session); + }, + [onDeleteClick, session] + ); + const handleCardClick = useCallback(() => { onSelectSession(session.id); }, [session.id]); @@ -380,16 +432,25 @@ const SessionListView: React.FC = React.memo( className="session-item h-full py-3 px-4 hover:shadow-default cursor-pointer transition-all duration-150 flex flex-col justify-between relative group" ref={(el) => setSessionRefs(session.id, el)} > - +
+ + +
-

+

{session.metadata.description || session.id}

@@ -505,7 +566,12 @@ const SessionListView: React.FC = React.memo(
{group.sessions.map((session) => ( - + ))}
@@ -605,6 +671,17 @@ const SessionListView: React.FC = React.memo( onClose={handleModalClose} onSave={handleModalSave} /> + + ); } diff --git a/ui/desktop/src/components/ui/ConfirmationModal.tsx b/ui/desktop/src/components/ui/ConfirmationModal.tsx index 3a07337156f6..e3add6562106 100644 --- a/ui/desktop/src/components/ui/ConfirmationModal.tsx +++ b/ui/desktop/src/components/ui/ConfirmationModal.tsx @@ -17,6 +17,7 @@ export function ConfirmationModal({ confirmLabel = 'Yes', cancelLabel = 'No', isSubmitting = false, + confirmVariant = 'default', }: { isOpen: boolean; title: string; @@ -26,20 +27,21 @@ export function ConfirmationModal({ confirmLabel?: string; cancelLabel?: string; isSubmitting?: boolean; // To handle debounce state + confirmVariant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'; }) { return ( !open && onCancel()}> {title} - {message} + {message} - diff --git a/ui/desktop/src/sessions.ts b/ui/desktop/src/sessions.ts index a19220945931..bb488b4ee8ab 100644 --- a/ui/desktop/src/sessions.ts +++ b/ui/desktop/src/sessions.ts @@ -151,3 +151,30 @@ export function resumeSession(session: SessionDetails | Session) { resumedSessionId ); } + +/** + * Deletes a specific session + * @param sessionId The ID of the session to delete + * @returns Promise that resolves when the deletion is complete + */ +export async function deleteSession(sessionId: string): Promise { + try { + const url = getApiUrl(`/sessions/${sessionId}/delete`); + const secretKey = await window.electron.getSecretKey(); + + const response = await fetch(url, { + method: 'DELETE', + headers: { + 'X-Secret-Key': secretKey, + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Failed to delete session: ${response.statusText} - ${errorText}`); + } + } catch (error) { + console.error(`Error deleting session ${sessionId}:`, error); + throw error; + } +}