feat: delete session from sidebar with confirmation dialog

This commit is contained in:
Nicolas Varrot
2026-02-12 16:57:18 +00:00
parent 94df362001
commit e94325b38a
5 changed files with 63 additions and 5 deletions

View File

@@ -268,7 +268,7 @@
## Item #30
- **Date:** 2026-02-12
- **Priority:** medium
- **Status:** pending
- **Status:** in-progress
- **Description:** Supprimer une session depuis la sidebar
- **Details:**
- Ajouter un bouton/action pour supprimer une session (clic droit ou icône)

View File

@@ -12,7 +12,7 @@ const Chat = lazy(() => import('./components/Chat').then(m => ({ default: m.Chat
export default function App() {
const {
status, messages, sessions, activeSession, isGenerating, isLoadingHistory,
sendMessage, abort, switchSession,
sendMessage, abort, switchSession, deleteSession,
authenticated, login, logout, connectError, isConnecting,
} = useGateway();
const [sidebarOpen, setSidebarOpen] = useState(false);
@@ -79,6 +79,7 @@ export default function App() {
sessions={sessions}
activeSession={activeSession}
onSwitch={switchSession}
onDelete={deleteSession}
open={sidebarOpen}
onClose={() => setSidebarOpen(false)}
/>

View File

@@ -1,5 +1,5 @@
import { useState, useMemo, useRef, useEffect, useCallback } from 'react';
import { X, Sparkles, Search, Pin } from 'lucide-react';
import { X, Sparkles, Search, Pin, Trash2 } from 'lucide-react';
import type { Session } from '../types';
import { useT } from '../hooks/useLocale';
import { SessionIcon } from './SessionIcon';
@@ -39,17 +39,19 @@ interface Props {
sessions: Session[];
activeSession: string;
onSwitch: (key: string) => void;
onDelete: (key: string) => void;
open: boolean;
onClose: () => void;
}
export function Sidebar({ sessions, activeSession, onSwitch, open, onClose }: Props) {
export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onClose }: Props) {
const t = useT();
const [filter, setFilter] = useState('');
const [focusIdx, setFocusIdx] = useState(-1);
const [pinned, setPinned] = useState(getPinnedSessions);
const [width, setWidth] = useState(getSavedWidth);
const [dragging, setDragging] = useState(false);
const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
const searchRef = useRef<HTMLInputElement>(null);
const listRef = useRef<HTMLDivElement>(null);
const dragRef = useRef({ startX: 0, startW: 0 });
@@ -260,6 +262,14 @@ export function Sidebar({ sessions, activeSession, onSwitch, open, onClose }: Pr
>
<Pin size={12} className={isPinned ? 'fill-current' : ''} />
</button>
<button
onClick={(e) => { e.stopPropagation(); setConfirmDelete(s.key); }}
className="shrink-0 p-0.5 rounded-lg transition-all text-zinc-600 opacity-0 group-hover/item:opacity-60 hover:!opacity-100 hover:text-red-400"
title={t('sidebar.delete')}
aria-label={t('sidebar.delete')}
>
<Trash2 size={12} />
</button>
{s.messageCount != null && (
<span className={`text-[11px] px-2 py-0.5 rounded-full shrink-0 ${isActive ? 'bg-cyan-400/10 text-cyan-300' : 'bg-white/5 text-zinc-500'}`}>
{s.messageCount}
@@ -310,6 +320,29 @@ export function Sidebar({ sessions, activeSession, onSwitch, open, onClose }: Pr
</aside>
{/* Prevent text selection while dragging */}
{dragging && <div className="fixed inset-0 z-[60] cursor-col-resize" />}
{/* Delete confirmation dialog */}
{confirmDelete && (
<>
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[70]" onClick={() => setConfirmDelete(null)} />
<div className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-[80] w-72 bg-[#1e1e24] border border-white/10 rounded-2xl p-5 shadow-2xl">
<p className="text-sm text-zinc-300 mb-4">{t('sidebar.deleteConfirm')}</p>
<div className="flex gap-2 justify-end">
<button
onClick={() => setConfirmDelete(null)}
className="px-3 py-1.5 text-xs rounded-xl border border-white/10 text-zinc-400 hover:bg-white/5 transition-colors"
>
{t('sidebar.deleteCancel')}
</button>
<button
onClick={() => { onDelete(confirmDelete); setConfirmDelete(null); }}
className="px-3 py-1.5 text-xs rounded-xl bg-red-500/20 text-red-300 border border-red-500/20 hover:bg-red-500/30 transition-colors"
>
{t('sidebar.delete')}
</button>
</div>
</div>
</>
)}
</>
);
}

View File

@@ -388,6 +388,24 @@ export function useGateway() {
setupClient(url, token);
}, [setupClient]);
const deleteSession = useCallback(async (key: string) => {
try {
await clientRef.current?.send('sessions.delete', { key, deleteTranscript: true });
} catch {
// Ignore delete failures
}
// Remove from local state
setSessions(prev => prev.filter(s => s.key !== key));
// If we deleted the active session, switch to main
if (activeSessionRef.current === key) {
const mainKey = 'agent:main:main';
setActiveSession(mainKey);
activeSessionRef.current = mainKey;
setMessages([]);
loadHistory(mainKey);
}
}, [loadHistory]);
const logout = useCallback(() => {
if (clientRef.current) {
clientRef.current.disconnect();
@@ -416,7 +434,7 @@ export function useGateway() {
return {
status, messages, sessions: enrichedSessions, activeSession, isGenerating, isLoadingHistory,
sendMessage, abort, switchSession, loadSessions,
sendMessage, abort, switchSession, loadSessions, deleteSession,
authenticated, login, logout, connectError, isConnecting,
};
}

View File

@@ -52,6 +52,9 @@ const en = {
'sidebar.pin': 'Pin session',
'sidebar.unpin': 'Unpin session',
'sidebar.pinned': 'Pinned',
'sidebar.delete': 'Delete session',
'sidebar.deleteConfirm': 'Delete this session? This cannot be undone.',
'sidebar.deleteCancel': 'Cancel',
// Thinking
'thinking.label': 'Thinking',
@@ -134,6 +137,9 @@ const fr: Record<keyof typeof en, string> = {
'sidebar.pin': 'Épingler la session',
'sidebar.unpin': 'Désépingler la session',
'sidebar.pinned': 'Épinglées',
'sidebar.delete': 'Supprimer la session',
'sidebar.deleteConfirm': 'Supprimer cette session ? Cette action est irréversible.',
'sidebar.deleteCancel': 'Annuler',
'thinking.label': 'Réflexion',