feat: delete session from sidebar with confirmation dialog
This commit is contained in:
@@ -268,7 +268,7 @@
|
|||||||
## Item #30
|
## Item #30
|
||||||
- **Date:** 2026-02-12
|
- **Date:** 2026-02-12
|
||||||
- **Priority:** medium
|
- **Priority:** medium
|
||||||
- **Status:** pending
|
- **Status:** in-progress
|
||||||
- **Description:** Supprimer une session depuis la sidebar
|
- **Description:** Supprimer une session depuis la sidebar
|
||||||
- **Details:**
|
- **Details:**
|
||||||
- Ajouter un bouton/action pour supprimer une session (clic droit ou icône)
|
- Ajouter un bouton/action pour supprimer une session (clic droit ou icône)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const Chat = lazy(() => import('./components/Chat').then(m => ({ default: m.Chat
|
|||||||
export default function App() {
|
export default function App() {
|
||||||
const {
|
const {
|
||||||
status, messages, sessions, activeSession, isGenerating, isLoadingHistory,
|
status, messages, sessions, activeSession, isGenerating, isLoadingHistory,
|
||||||
sendMessage, abort, switchSession,
|
sendMessage, abort, switchSession, deleteSession,
|
||||||
authenticated, login, logout, connectError, isConnecting,
|
authenticated, login, logout, connectError, isConnecting,
|
||||||
} = useGateway();
|
} = useGateway();
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
@@ -79,6 +79,7 @@ export default function App() {
|
|||||||
sessions={sessions}
|
sessions={sessions}
|
||||||
activeSession={activeSession}
|
activeSession={activeSession}
|
||||||
onSwitch={switchSession}
|
onSwitch={switchSession}
|
||||||
|
onDelete={deleteSession}
|
||||||
open={sidebarOpen}
|
open={sidebarOpen}
|
||||||
onClose={() => setSidebarOpen(false)}
|
onClose={() => setSidebarOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useMemo, useRef, useEffect, useCallback } from 'react';
|
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 type { Session } from '../types';
|
||||||
import { useT } from '../hooks/useLocale';
|
import { useT } from '../hooks/useLocale';
|
||||||
import { SessionIcon } from './SessionIcon';
|
import { SessionIcon } from './SessionIcon';
|
||||||
@@ -39,17 +39,19 @@ interface Props {
|
|||||||
sessions: Session[];
|
sessions: Session[];
|
||||||
activeSession: string;
|
activeSession: string;
|
||||||
onSwitch: (key: string) => void;
|
onSwitch: (key: string) => void;
|
||||||
|
onDelete: (key: string) => void;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
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 t = useT();
|
||||||
const [filter, setFilter] = useState('');
|
const [filter, setFilter] = useState('');
|
||||||
const [focusIdx, setFocusIdx] = useState(-1);
|
const [focusIdx, setFocusIdx] = useState(-1);
|
||||||
const [pinned, setPinned] = useState(getPinnedSessions);
|
const [pinned, setPinned] = useState(getPinnedSessions);
|
||||||
const [width, setWidth] = useState(getSavedWidth);
|
const [width, setWidth] = useState(getSavedWidth);
|
||||||
const [dragging, setDragging] = useState(false);
|
const [dragging, setDragging] = useState(false);
|
||||||
|
const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
|
||||||
const searchRef = useRef<HTMLInputElement>(null);
|
const searchRef = useRef<HTMLInputElement>(null);
|
||||||
const listRef = useRef<HTMLDivElement>(null);
|
const listRef = useRef<HTMLDivElement>(null);
|
||||||
const dragRef = useRef({ startX: 0, startW: 0 });
|
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' : ''} />
|
<Pin size={12} className={isPinned ? 'fill-current' : ''} />
|
||||||
</button>
|
</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 && (
|
{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'}`}>
|
<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}
|
{s.messageCount}
|
||||||
@@ -310,6 +320,29 @@ export function Sidebar({ sessions, activeSession, onSwitch, open, onClose }: Pr
|
|||||||
</aside>
|
</aside>
|
||||||
{/* Prevent text selection while dragging */}
|
{/* Prevent text selection while dragging */}
|
||||||
{dragging && <div className="fixed inset-0 z-[60] cursor-col-resize" />}
|
{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>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -388,6 +388,24 @@ export function useGateway() {
|
|||||||
setupClient(url, token);
|
setupClient(url, token);
|
||||||
}, [setupClient]);
|
}, [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(() => {
|
const logout = useCallback(() => {
|
||||||
if (clientRef.current) {
|
if (clientRef.current) {
|
||||||
clientRef.current.disconnect();
|
clientRef.current.disconnect();
|
||||||
@@ -416,7 +434,7 @@ export function useGateway() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
status, messages, sessions: enrichedSessions, activeSession, isGenerating, isLoadingHistory,
|
status, messages, sessions: enrichedSessions, activeSession, isGenerating, isLoadingHistory,
|
||||||
sendMessage, abort, switchSession, loadSessions,
|
sendMessage, abort, switchSession, loadSessions, deleteSession,
|
||||||
authenticated, login, logout, connectError, isConnecting,
|
authenticated, login, logout, connectError, isConnecting,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ const en = {
|
|||||||
'sidebar.pin': 'Pin session',
|
'sidebar.pin': 'Pin session',
|
||||||
'sidebar.unpin': 'Unpin session',
|
'sidebar.unpin': 'Unpin session',
|
||||||
'sidebar.pinned': 'Pinned',
|
'sidebar.pinned': 'Pinned',
|
||||||
|
'sidebar.delete': 'Delete session',
|
||||||
|
'sidebar.deleteConfirm': 'Delete this session? This cannot be undone.',
|
||||||
|
'sidebar.deleteCancel': 'Cancel',
|
||||||
|
|
||||||
// Thinking
|
// Thinking
|
||||||
'thinking.label': 'Thinking',
|
'thinking.label': 'Thinking',
|
||||||
@@ -134,6 +137,9 @@ const fr: Record<keyof typeof en, string> = {
|
|||||||
'sidebar.pin': 'Épingler la session',
|
'sidebar.pin': 'Épingler la session',
|
||||||
'sidebar.unpin': 'Désépingler la session',
|
'sidebar.unpin': 'Désépingler la session',
|
||||||
'sidebar.pinned': 'Épinglées',
|
'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',
|
'thinking.label': 'Réflexion',
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user