feat: Add functionality to delete session in history list view (#4480)
Signed-off-by: Abhijay007 <Abhijay007j@gmail.com> Co-authored-by: Zane Staggs <zane@squareup.com>
This commit is contained in:
@@ -7,7 +7,7 @@ use crate::state::AppState;
|
|||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
http::{HeaderMap, StatusCode},
|
http::{HeaderMap, StatusCode},
|
||||||
routing::{get, put},
|
routing::{delete, get, put},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use goose::conversation::message::Message;
|
use goose::conversation::message::Message;
|
||||||
@@ -310,11 +310,54 @@ async fn update_session_metadata(
|
|||||||
Ok(StatusCode::OK)
|
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<Arc<AppState>>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path(session_id): Path<String>,
|
||||||
|
) -> Result<StatusCode, StatusCode> {
|
||||||
|
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
|
// Configure routes for this module
|
||||||
pub fn routes(state: Arc<AppState>) -> Router {
|
pub fn routes(state: Arc<AppState>) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/sessions", get(list_sessions))
|
.route("/sessions", get(list_sessions))
|
||||||
.route("/sessions/{session_id}", get(get_session_history))
|
.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/insights", get(get_session_insights))
|
||||||
.route(
|
.route(
|
||||||
"/sessions/{session_id}/metadata",
|
"/sessions/{session_id}/metadata",
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import React, { useEffect, useState, useRef, useCallback, useMemo, startTransition } from 'react';
|
import React, { useEffect, useState, useRef, useCallback, useMemo, startTransition } from 'react';
|
||||||
import { MessageSquareText, Target, AlertCircle, Calendar, Folder, Edit2 } from 'lucide-react';
|
import {
|
||||||
import { fetchSessions, updateSessionMetadata, type Session } from '../../sessions';
|
MessageSquareText,
|
||||||
|
Target,
|
||||||
|
AlertCircle,
|
||||||
|
Calendar,
|
||||||
|
Folder,
|
||||||
|
Edit2,
|
||||||
|
Trash2,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { fetchSessions, updateSessionMetadata, deleteSession, type Session } from '../../sessions';
|
||||||
import { Card } from '../ui/card';
|
import { Card } from '../ui/card';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { ScrollArea } from '../ui/scroll-area';
|
import { ScrollArea } from '../ui/scroll-area';
|
||||||
@@ -12,6 +20,7 @@ import { MainPanelLayout } from '../Layout/MainPanelLayout';
|
|||||||
import { groupSessionsByDate, type DateGroup } from '../../utils/dateUtils';
|
import { groupSessionsByDate, type DateGroup } from '../../utils/dateUtils';
|
||||||
import { Skeleton } from '../ui/skeleton';
|
import { Skeleton } from '../ui/skeleton';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
import { ConfirmationModal } from '../ui/ConfirmationModal';
|
||||||
|
|
||||||
interface EditSessionModalProps {
|
interface EditSessionModalProps {
|
||||||
session: Session | null;
|
session: Session | null;
|
||||||
@@ -177,6 +186,10 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
|||||||
const [showEditModal, setShowEditModal] = useState(false);
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
const [editingSession, setEditingSession] = useState<Session | null>(null);
|
const [editingSession, setEditingSession] = useState<Session | null>(null);
|
||||||
|
|
||||||
|
// Delete confirmation modal state
|
||||||
|
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
|
||||||
|
const [sessionToDelete, setSessionToDelete] = useState<Session | null>(null);
|
||||||
|
|
||||||
// Search state for debouncing
|
// Search state for debouncing
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [caseSensitive, setCaseSensitive] = useState(false);
|
const [caseSensitive, setCaseSensitive] = useState(false);
|
||||||
@@ -355,12 +368,43 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
|||||||
setShowEditModal(true);
|
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({
|
const SessionItem = React.memo(function SessionItem({
|
||||||
session,
|
session,
|
||||||
onEditClick,
|
onEditClick,
|
||||||
|
onDeleteClick,
|
||||||
}: {
|
}: {
|
||||||
session: Session;
|
session: Session;
|
||||||
onEditClick: (session: Session) => void;
|
onEditClick: (session: Session) => void;
|
||||||
|
onDeleteClick: (session: Session) => void;
|
||||||
}) {
|
}) {
|
||||||
const handleEditClick = useCallback(
|
const handleEditClick = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
@@ -370,6 +414,14 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
|||||||
[onEditClick, session]
|
[onEditClick, session]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleDeleteClick = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation(); // Prevent card click
|
||||||
|
onDeleteClick(session);
|
||||||
|
},
|
||||||
|
[onDeleteClick, session]
|
||||||
|
);
|
||||||
|
|
||||||
const handleCardClick = useCallback(() => {
|
const handleCardClick = useCallback(() => {
|
||||||
onSelectSession(session.id);
|
onSelectSession(session.id);
|
||||||
}, [session.id]);
|
}, [session.id]);
|
||||||
@@ -380,16 +432,25 @@ const SessionListView: React.FC<SessionListViewProps> = 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"
|
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)}
|
ref={(el) => setSessionRefs(session.id, el)}
|
||||||
>
|
>
|
||||||
<button
|
<div className="absolute top-3 right-4 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
onClick={handleEditClick}
|
<button
|
||||||
className="absolute top-3 right-4 p-2 rounded opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer"
|
onClick={handleEditClick}
|
||||||
title="Edit session name"
|
className="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer"
|
||||||
>
|
title="Edit session name"
|
||||||
<Edit2 className="w-3 h-3 text-textSubtle hover:text-textStandard" />
|
>
|
||||||
</button>
|
<Edit2 className="w-3 h-3 text-textSubtle hover:text-textStandard" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleDeleteClick}
|
||||||
|
className="p-2 rounded hover:bg-red-50 dark:hover:bg-red-900/20 cursor-pointer transition-colors"
|
||||||
|
title="Delete session"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3 h-3 text-red-500 hover:text-red-600" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h3 className="text-base mb-1 pr-6 break-words">
|
<h3 className="text-base mb-1 pr-16 break-words">
|
||||||
{session.metadata.description || session.id}
|
{session.metadata.description || session.id}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
@@ -505,7 +566,12 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||||
{group.sessions.map((session) => (
|
{group.sessions.map((session) => (
|
||||||
<SessionItem key={session.id} session={session} onEditClick={handleEditSession} />
|
<SessionItem
|
||||||
|
key={session.id}
|
||||||
|
session={session}
|
||||||
|
onEditClick={handleEditSession}
|
||||||
|
onDeleteClick={handleDeleteSession}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -605,6 +671,17 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
|||||||
onClose={handleModalClose}
|
onClose={handleModalClose}
|
||||||
onSave={handleModalSave}
|
onSave={handleModalSave}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ConfirmationModal
|
||||||
|
isOpen={showDeleteConfirmation}
|
||||||
|
title="Delete Session"
|
||||||
|
message={`Are you sure you want to delete the session "${sessionToDelete?.metadata.description || sessionToDelete?.id}"? This action cannot be undone.`}
|
||||||
|
confirmLabel="Delete Session"
|
||||||
|
cancelLabel="Cancel"
|
||||||
|
confirmVariant="destructive"
|
||||||
|
onConfirm={handleConfirmDelete}
|
||||||
|
onCancel={handleCancelDelete}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export function ConfirmationModal({
|
|||||||
confirmLabel = 'Yes',
|
confirmLabel = 'Yes',
|
||||||
cancelLabel = 'No',
|
cancelLabel = 'No',
|
||||||
isSubmitting = false,
|
isSubmitting = false,
|
||||||
|
confirmVariant = 'default',
|
||||||
}: {
|
}: {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -26,20 +27,21 @@ export function ConfirmationModal({
|
|||||||
confirmLabel?: string;
|
confirmLabel?: string;
|
||||||
cancelLabel?: string;
|
cancelLabel?: string;
|
||||||
isSubmitting?: boolean; // To handle debounce state
|
isSubmitting?: boolean; // To handle debounce state
|
||||||
|
confirmVariant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={(open) => !open && onCancel()}>
|
<Dialog open={isOpen} onOpenChange={(open) => !open && onCancel()}>
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{title}</DialogTitle>
|
<DialogTitle>{title}</DialogTitle>
|
||||||
<DialogDescription className="whitespace-pre-wrap">{message}</DialogDescription>
|
<DialogDescription>{message}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<DialogFooter className="pt-2">
|
<DialogFooter className="pt-2">
|
||||||
<Button variant="outline" onClick={onCancel} disabled={isSubmitting}>
|
<Button variant="outline" onClick={onCancel} disabled={isSubmitting}>
|
||||||
{cancelLabel}
|
{cancelLabel}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onConfirm} disabled={isSubmitting}>
|
<Button variant={confirmVariant} onClick={onConfirm} disabled={isSubmitting}>
|
||||||
{isSubmitting ? 'Processing...' : confirmLabel}
|
{isSubmitting ? 'Processing...' : confirmLabel}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -151,3 +151,30 @@ export function resumeSession(session: SessionDetails | Session) {
|
|||||||
resumedSessionId
|
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<void> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user