mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-02-23 17:17:41 +00:00
Merge pull request #314 from EricBlanquer/feature/delete-project-with-sessions
feat: allow deleting projects with sessions and add styled confirmation modal
This commit is contained in:
@@ -455,11 +455,12 @@ app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken,
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete project endpoint (only if empty)
|
// Delete project endpoint (force=true to delete with sessions)
|
||||||
app.delete('/api/projects/:projectName', authenticateToken, async (req, res) => {
|
app.delete('/api/projects/:projectName', authenticateToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { projectName } = req.params;
|
const { projectName } = req.params;
|
||||||
await deleteProject(projectName);
|
const force = req.query.force === 'true';
|
||||||
|
await deleteProject(projectName, force);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
|
|||||||
@@ -1026,25 +1026,56 @@ async function isProjectEmpty(projectName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete an empty project
|
// Delete a project (force=true to delete even with sessions)
|
||||||
async function deleteProject(projectName) {
|
async function deleteProject(projectName, force = false) {
|
||||||
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
|
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// First check if the project is empty
|
|
||||||
const isEmpty = await isProjectEmpty(projectName);
|
const isEmpty = await isProjectEmpty(projectName);
|
||||||
if (!isEmpty) {
|
if (!isEmpty && !force) {
|
||||||
throw new Error('Cannot delete project with existing sessions');
|
throw new Error('Cannot delete project with existing sessions');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the project directory
|
|
||||||
await fs.rm(projectDir, { recursive: true, force: true });
|
|
||||||
|
|
||||||
// Remove from project config
|
|
||||||
const config = await loadProjectConfig();
|
const config = await loadProjectConfig();
|
||||||
|
let projectPath = config[projectName]?.path || config[projectName]?.originalPath;
|
||||||
|
|
||||||
|
// Fallback to extractProjectDirectory if projectPath is not in config
|
||||||
|
if (!projectPath) {
|
||||||
|
projectPath = await extractProjectDirectory(projectName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the project directory (includes all Claude sessions)
|
||||||
|
await fs.rm(projectDir, { recursive: true, force: true });
|
||||||
|
|
||||||
|
// Delete all Codex sessions associated with this project
|
||||||
|
if (projectPath) {
|
||||||
|
try {
|
||||||
|
const codexSessions = await getCodexSessions(projectPath, { limit: 0 });
|
||||||
|
for (const session of codexSessions) {
|
||||||
|
try {
|
||||||
|
await deleteCodexSession(session.id);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`Failed to delete Codex session ${session.id}:`, err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to delete Codex sessions:', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete Cursor sessions directory if it exists
|
||||||
|
try {
|
||||||
|
const hash = crypto.createHash('md5').update(projectPath).digest('hex');
|
||||||
|
const cursorProjectDir = path.join(os.homedir(), '.cursor', 'chats', hash);
|
||||||
|
await fs.rm(cursorProjectDir, { recursive: true, force: true });
|
||||||
|
} catch (err) {
|
||||||
|
// Cursor dir may not exist, ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from project config
|
||||||
delete config[projectName];
|
delete config[projectName];
|
||||||
await saveProjectConfig(config);
|
await saveProjectConfig(config);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error deleting project ${projectName}:`, error);
|
console.error(`Error deleting project ${projectName}:`, error);
|
||||||
@@ -1055,17 +1086,17 @@ async function deleteProject(projectName) {
|
|||||||
// Add a project manually to the config (without creating folders)
|
// Add a project manually to the config (without creating folders)
|
||||||
async function addProjectManually(projectPath, displayName = null) {
|
async function addProjectManually(projectPath, displayName = null) {
|
||||||
const absolutePath = path.resolve(projectPath);
|
const absolutePath = path.resolve(projectPath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if the path exists
|
// Check if the path exists
|
||||||
await fs.access(absolutePath);
|
await fs.access(absolutePath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Path does not exist: ${absolutePath}`);
|
throw new Error(`Path does not exist: ${absolutePath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate project name (encode path for use as directory name)
|
// Generate project name (encode path for use as directory name)
|
||||||
const projectName = absolutePath.replace(/\//g, '-');
|
const projectName = absolutePath.replace(/\//g, '-');
|
||||||
|
|
||||||
// Check if project already exists in config
|
// Check if project already exists in config
|
||||||
const config = await loadProjectConfig();
|
const config = await loadProjectConfig();
|
||||||
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
|
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
|
||||||
@@ -1076,13 +1107,13 @@ async function addProjectManually(projectPath, displayName = null) {
|
|||||||
|
|
||||||
// Allow adding projects even if the directory exists - this enables tracking
|
// Allow adding projects even if the directory exists - this enables tracking
|
||||||
// existing Claude Code or Cursor projects in the UI
|
// existing Claude Code or Cursor projects in the UI
|
||||||
|
|
||||||
// Add to config as manually added project
|
// Add to config as manually added project
|
||||||
config[projectName] = {
|
config[projectName] = {
|
||||||
manuallyAdded: true,
|
manuallyAdded: true,
|
||||||
originalPath: absolutePath
|
originalPath: absolutePath
|
||||||
};
|
};
|
||||||
|
|
||||||
if (displayName) {
|
if (displayName) {
|
||||||
config[projectName].displayName = displayName;
|
config[projectName].displayName = displayName;
|
||||||
}
|
}
|
||||||
@@ -1214,7 +1245,8 @@ async function getCursorSessions(projectPath) {
|
|||||||
|
|
||||||
|
|
||||||
// Fetch Codex sessions for a given project path
|
// Fetch Codex sessions for a given project path
|
||||||
async function getCodexSessions(projectPath) {
|
async function getCodexSessions(projectPath, options = {}) {
|
||||||
|
const { limit = 5 } = options;
|
||||||
try {
|
try {
|
||||||
const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
|
const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
|
||||||
const sessions = [];
|
const sessions = [];
|
||||||
@@ -1279,8 +1311,8 @@ async function getCodexSessions(projectPath) {
|
|||||||
// Sort sessions by last activity (newest first)
|
// Sort sessions by last activity (newest first)
|
||||||
sessions.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
|
sessions.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
|
||||||
|
|
||||||
// Return only the first 5 sessions for performance
|
// Return limited sessions for performance (0 = unlimited for deletion)
|
||||||
return sessions.slice(0, 5);
|
return limit > 0 ? sessions.slice(0, limit) : sessions;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching Codex sessions:', error);
|
console.error('Error fetching Codex sessions:', error);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Badge } from './ui/badge';
|
|||||||
import { Input } from './ui/input';
|
import { Input } from './ui/input';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { FolderOpen, Folder, Plus, MessageSquare, Clock, ChevronDown, ChevronRight, Edit3, Check, X, Trash2, Settings, FolderPlus, RefreshCw, Sparkles, Edit2, Star, Search } from 'lucide-react';
|
import { FolderOpen, Folder, Plus, MessageSquare, Clock, ChevronDown, ChevronRight, Edit3, Check, X, Trash2, Settings, FolderPlus, RefreshCw, Sparkles, Edit2, Star, Search, AlertTriangle } from 'lucide-react';
|
||||||
import { cn } from '../lib/utils';
|
import { cn } from '../lib/utils';
|
||||||
import ClaudeLogo from './ClaudeLogo';
|
import ClaudeLogo from './ClaudeLogo';
|
||||||
import CursorLogo from './CursorLogo.jsx';
|
import CursorLogo from './CursorLogo.jsx';
|
||||||
@@ -80,6 +80,9 @@ function Sidebar({
|
|||||||
const [editingSessionName, setEditingSessionName] = useState('');
|
const [editingSessionName, setEditingSessionName] = useState('');
|
||||||
const [generatingSummary, setGeneratingSummary] = useState({});
|
const [generatingSummary, setGeneratingSummary] = useState({});
|
||||||
const [searchFilter, setSearchFilter] = useState('');
|
const [searchFilter, setSearchFilter] = useState('');
|
||||||
|
const [deletingProjects, setDeletingProjects] = useState(new Set());
|
||||||
|
const [deleteConfirmation, setDeleteConfirmation] = useState(null); // { project, sessionCount }
|
||||||
|
const [sessionDeleteConfirmation, setSessionDeleteConfirmation] = useState(null); // { projectName, sessionId, sessionTitle, provider }
|
||||||
|
|
||||||
// TaskMaster context
|
// TaskMaster context
|
||||||
const { setCurrentProject, mcpServerStatus } = useTaskMaster();
|
const { setCurrentProject, mcpServerStatus } = useTaskMaster();
|
||||||
@@ -306,10 +309,15 @@ function Sidebar({
|
|||||||
setEditingName('');
|
setEditingName('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteSession = async (projectName, sessionId, provider = 'claude') => {
|
const showDeleteSessionConfirmation = (projectName, sessionId, sessionTitle, provider = 'claude') => {
|
||||||
if (!confirm(t('messages.deleteSessionConfirm'))) {
|
setSessionDeleteConfirmation({ projectName, sessionId, sessionTitle, provider });
|
||||||
return;
|
};
|
||||||
}
|
|
||||||
|
const confirmDeleteSession = async () => {
|
||||||
|
if (!sessionDeleteConfirmation) return;
|
||||||
|
|
||||||
|
const { projectName, sessionId, provider } = sessionDeleteConfirmation;
|
||||||
|
setSessionDeleteConfirmation(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('[Sidebar] Deleting session:', { projectName, sessionId, provider });
|
console.log('[Sidebar] Deleting session:', { projectName, sessionId, provider });
|
||||||
@@ -343,18 +351,26 @@ function Sidebar({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteProject = async (projectName) => {
|
const deleteProject = (project) => {
|
||||||
if (!confirm(t('messages.deleteProjectConfirm'))) {
|
const sessionCount = getAllSessions(project).length;
|
||||||
return;
|
setDeleteConfirmation({ project, sessionCount });
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const confirmDeleteProject = async () => {
|
||||||
|
if (!deleteConfirmation) return;
|
||||||
|
|
||||||
|
const { project, sessionCount } = deleteConfirmation;
|
||||||
|
const isEmpty = sessionCount === 0;
|
||||||
|
|
||||||
|
setDeleteConfirmation(null);
|
||||||
|
setDeletingProjects(prev => new Set([...prev, project.name]));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.deleteProject(projectName);
|
const response = await api.deleteProject(project.name, !isEmpty);
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
// Call parent callback if provided
|
|
||||||
if (onProjectDelete) {
|
if (onProjectDelete) {
|
||||||
onProjectDelete(projectName);
|
onProjectDelete(project.name);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
@@ -364,6 +380,12 @@ function Sidebar({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting project:', error);
|
console.error('Error deleting project:', error);
|
||||||
alert(t('messages.deleteProjectError'));
|
alert(t('messages.deleteProjectError'));
|
||||||
|
} finally {
|
||||||
|
setDeletingProjects(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(project.name);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -488,6 +510,110 @@ function Sidebar({
|
|||||||
document.body
|
document.body
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Delete Confirmation Modal */}
|
||||||
|
{deleteConfirmation && ReactDOM.createPortal(
|
||||||
|
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-card border border-border rounded-xl shadow-2xl max-w-md w-full overflow-hidden">
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center flex-shrink-0">
|
||||||
|
<AlertTriangle className="w-6 h-6 text-red-600 dark:text-red-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="text-lg font-semibold text-foreground mb-2">
|
||||||
|
{t('deleteConfirmation.deleteProject')}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-1">
|
||||||
|
{t('deleteConfirmation.confirmDelete')}{' '}
|
||||||
|
<span className="font-medium text-foreground">
|
||||||
|
{deleteConfirmation.project.displayName || deleteConfirmation.project.name}
|
||||||
|
</span>?
|
||||||
|
</p>
|
||||||
|
{deleteConfirmation.sessionCount > 0 && (
|
||||||
|
<div className="mt-3 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||||
|
<p className="text-sm text-red-700 dark:text-red-300 font-medium">
|
||||||
|
{t('deleteConfirmation.sessionCount', { count: deleteConfirmation.sessionCount })}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-red-600 dark:text-red-400 mt-1">
|
||||||
|
{t('deleteConfirmation.allConversationsDeleted')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-muted-foreground mt-3">
|
||||||
|
{t('deleteConfirmation.cannotUndo')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3 p-4 bg-muted/30 border-t border-border">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
onClick={() => setDeleteConfirmation(null)}
|
||||||
|
>
|
||||||
|
{t('actions.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
className="flex-1 bg-red-600 hover:bg-red-700 text-white"
|
||||||
|
onClick={confirmDeleteProject}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 mr-2" />
|
||||||
|
{t('actions.delete')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Session Delete Confirmation Modal */}
|
||||||
|
{sessionDeleteConfirmation && ReactDOM.createPortal(
|
||||||
|
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-card border border-border rounded-xl shadow-2xl max-w-md w-full overflow-hidden">
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center flex-shrink-0">
|
||||||
|
<AlertTriangle className="w-6 h-6 text-red-600 dark:text-red-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="text-lg font-semibold text-foreground mb-2">
|
||||||
|
{t('deleteConfirmation.deleteSession')}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-1">
|
||||||
|
{t('deleteConfirmation.confirmDelete')}{' '}
|
||||||
|
<span className="font-medium text-foreground">
|
||||||
|
{sessionDeleteConfirmation.sessionTitle || t('sessions.unnamed')}
|
||||||
|
</span>?
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-3">
|
||||||
|
{t('deleteConfirmation.cannotUndo')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3 p-4 bg-muted/30 border-t border-border">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
onClick={() => setSessionDeleteConfirmation(null)}
|
||||||
|
>
|
||||||
|
{t('actions.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
className="flex-1 bg-red-600 hover:bg-red-700 text-white"
|
||||||
|
onClick={confirmDeleteSession}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 mr-2" />
|
||||||
|
{t('actions.delete')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="h-full flex flex-col bg-card md:select-none"
|
className="h-full flex flex-col bg-card md:select-none"
|
||||||
style={isPWA && isMobile ? { paddingTop: '44px' } : {}}
|
style={isPWA && isMobile ? { paddingTop: '44px' } : {}}
|
||||||
@@ -718,9 +844,10 @@ function Sidebar({
|
|||||||
const isExpanded = expandedProjects.has(project.name);
|
const isExpanded = expandedProjects.has(project.name);
|
||||||
const isSelected = selectedProject?.name === project.name;
|
const isSelected = selectedProject?.name === project.name;
|
||||||
const isStarred = isProjectStarred(project.name);
|
const isStarred = isProjectStarred(project.name);
|
||||||
|
const isDeleting = deletingProjects.has(project.name);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={project.name} className="md:space-y-1">
|
<div key={project.name} className={cn("md:space-y-1", isDeleting && "opacity-50 pointer-events-none")}>
|
||||||
{/* Project Header */}
|
{/* Project Header */}
|
||||||
<div className="group md:group">
|
<div className="group md:group">
|
||||||
{/* Mobile Project Item */}
|
{/* Mobile Project Item */}
|
||||||
@@ -849,18 +976,16 @@ function Sidebar({
|
|||||||
: "text-gray-600 dark:text-gray-400"
|
: "text-gray-600 dark:text-gray-400"
|
||||||
)} />
|
)} />
|
||||||
</button>
|
</button>
|
||||||
{getAllSessions(project).length === 0 && (
|
<button
|
||||||
<button
|
|
||||||
className="w-8 h-8 rounded-lg bg-red-500/10 dark:bg-red-900/30 flex items-center justify-center active:scale-90 border border-red-200 dark:border-red-800"
|
className="w-8 h-8 rounded-lg bg-red-500/10 dark:bg-red-900/30 flex items-center justify-center active:scale-90 border border-red-200 dark:border-red-800"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
deleteProject(project.name);
|
deleteProject(project);
|
||||||
}}
|
}}
|
||||||
onTouchEnd={handleTouchClick(() => deleteProject(project.name))}
|
onTouchEnd={handleTouchClick(() => deleteProject(project))}
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4 text-red-600 dark:text-red-400" />
|
<Trash2 className="w-4 h-4 text-red-600 dark:text-red-400" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
className="w-8 h-8 rounded-lg bg-primary/10 dark:bg-primary/20 flex items-center justify-center active:scale-90 border border-primary/20 dark:border-primary/30"
|
className="w-8 h-8 rounded-lg bg-primary/10 dark:bg-primary/20 flex items-center justify-center active:scale-90 border border-primary/20 dark:border-primary/30"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -1009,18 +1134,16 @@ function Sidebar({
|
|||||||
>
|
>
|
||||||
<Edit3 className="w-3 h-3" />
|
<Edit3 className="w-3 h-3" />
|
||||||
</div>
|
</div>
|
||||||
{getAllSessions(project).length === 0 && (
|
<div
|
||||||
<div
|
|
||||||
className="w-6 h-6 opacity-0 group-hover:opacity-100 transition-all duration-200 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center justify-center rounded cursor-pointer touch:opacity-100"
|
className="w-6 h-6 opacity-0 group-hover:opacity-100 transition-all duration-200 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center justify-center rounded cursor-pointer touch:opacity-100"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
deleteProject(project.name);
|
deleteProject(project);
|
||||||
}}
|
}}
|
||||||
title={t('tooltips.deleteProject')}
|
title={t('tooltips.deleteProject')}
|
||||||
>
|
>
|
||||||
<Trash2 className="w-3 h-3 text-red-600 dark:text-red-400" />
|
<Trash2 className="w-3 h-3 text-red-600 dark:text-red-400" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<ChevronDown className="w-4 h-4 text-muted-foreground group-hover:text-foreground transition-colors" />
|
<ChevronDown className="w-4 h-4 text-muted-foreground group-hover:text-foreground transition-colors" />
|
||||||
) : (
|
) : (
|
||||||
@@ -1152,9 +1275,9 @@ function Sidebar({
|
|||||||
className="w-5 h-5 rounded-md bg-red-50 dark:bg-red-900/20 flex items-center justify-center active:scale-95 transition-transform opacity-70 ml-1"
|
className="w-5 h-5 rounded-md bg-red-50 dark:bg-red-900/20 flex items-center justify-center active:scale-95 transition-transform opacity-70 ml-1"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
deleteSession(project.name, session.id, session.__provider);
|
showDeleteSessionConfirmation(project.name, session.id, sessionName, session.__provider);
|
||||||
}}
|
}}
|
||||||
onTouchEnd={handleTouchClick(() => deleteSession(project.name, session.id, session.__provider))}
|
onTouchEnd={handleTouchClick(() => showDeleteSessionConfirmation(project.name, session.id, sessionName, session.__provider))}
|
||||||
>
|
>
|
||||||
<Trash2 className="w-2.5 h-2.5 text-red-600 dark:text-red-400" />
|
<Trash2 className="w-2.5 h-2.5 text-red-600 dark:text-red-400" />
|
||||||
</button>
|
</button>
|
||||||
@@ -1271,7 +1394,7 @@ function Sidebar({
|
|||||||
className="w-6 h-6 bg-red-50 hover:bg-red-100 dark:bg-red-900/20 dark:hover:bg-red-900/40 rounded flex items-center justify-center"
|
className="w-6 h-6 bg-red-50 hover:bg-red-100 dark:bg-red-900/20 dark:hover:bg-red-900/40 rounded flex items-center justify-center"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
deleteSession(project.name, session.id, session.__provider);
|
showDeleteSessionConfirmation(project.name, session.id, sessionName, session.__provider);
|
||||||
}}
|
}}
|
||||||
title={t('tooltips.deleteSession')}
|
title={t('tooltips.deleteSession')}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -98,5 +98,14 @@
|
|||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
"updateAvailable": "Update available"
|
"updateAvailable": "Update available"
|
||||||
|
},
|
||||||
|
"deleteConfirmation": {
|
||||||
|
"deleteProject": "Delete Project",
|
||||||
|
"deleteSession": "Delete Session",
|
||||||
|
"confirmDelete": "Are you sure you want to delete",
|
||||||
|
"sessionCount_one": "This project contains {{count}} conversation.",
|
||||||
|
"sessionCount_other": "This project contains {{count}} conversations.",
|
||||||
|
"allConversationsDeleted": "All conversations will be permanently deleted.",
|
||||||
|
"cannotUndo": "This action cannot be undone."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,5 +98,14 @@
|
|||||||
},
|
},
|
||||||
"version": {
|
"version": {
|
||||||
"updateAvailable": "有可用更新"
|
"updateAvailable": "有可用更新"
|
||||||
|
},
|
||||||
|
"deleteConfirmation": {
|
||||||
|
"deleteProject": "删除项目",
|
||||||
|
"deleteSession": "删除会话",
|
||||||
|
"confirmDelete": "您确定要删除",
|
||||||
|
"sessionCount_one": "此项目包含 {{count}} 个对话。",
|
||||||
|
"sessionCount_other": "此项目包含 {{count}} 个对话。",
|
||||||
|
"allConversationsDeleted": "所有对话将被永久删除。",
|
||||||
|
"cannotUndo": "此操作无法撤销。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,8 +79,8 @@ export const api = {
|
|||||||
authenticatedFetch(`/api/codex/sessions/${sessionId}`, {
|
authenticatedFetch(`/api/codex/sessions/${sessionId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
}),
|
}),
|
||||||
deleteProject: (projectName) =>
|
deleteProject: (projectName, force = false) =>
|
||||||
authenticatedFetch(`/api/projects/${projectName}`, {
|
authenticatedFetch(`/api/projects/${projectName}${force ? '?force=true' : ''}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
}),
|
}),
|
||||||
createProject: (path) =>
|
createProject: (path) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user