Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 94 additions & 1 deletion ui/desktop/src/components/sessions/SessionListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import {
Calendar,
ChevronRight,
Folder,
Trash2,
} from 'lucide-react';
import { fetchSessions, type Session } from '../../sessions';
import { fetchSessions, type Session, deleteSession } from '../../sessions';
import { Card } from '../ui/card';
import { Button } from '../ui/button';
import BackButton from '../ui/BackButton';
Expand All @@ -18,6 +19,8 @@ import { formatMessageTimestamp } from '../../utils/timeUtils';
import MoreMenuLayout from '../more_menu/MoreMenuLayout';
import { SearchView } from '../conversation/SearchView';
import { SearchHighlighter } from '../../utils/searchHighlighter';
import { toast } from 'react-toastify';
import { Modal, ModalContent } from '../ui/modal';

interface SearchContainerElement extends HTMLDivElement {
_searchHighlighter: SearchHighlighter | null;
Expand All @@ -42,6 +45,8 @@ const SessionListView: React.FC<SessionListViewProps> = ({ setView, onSelectSess
} | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [visibleRange, setVisibleRange] = useState({ start: 0, end: 20 });
const [sessionToDelete, setSessionToDelete] = useState<Session | null>(null);
const [isDeleting, setIsDeleting] = useState(false);

useEffect(() => {
loadSessions();
Expand Down Expand Up @@ -157,8 +162,37 @@ const SessionListView: React.FC<SessionListViewProps> = ({ setView, onSelectSess
}
};

// Handle delete confirmation
const handleDeleteConfirm = async () => {
if (!sessionToDelete) return;

setIsDeleting(true);
try {
await deleteSession(sessionToDelete.id);

// Remove the deleted session from the lists
const updatedSessions = sessions.filter(session => session.id !== sessionToDelete.id);
setSessions(updatedSessions);
setFilteredSessions(filteredSessions.filter(session => session.id !== sessionToDelete.id));

toast.success(`Session "${sessionToDelete.metadata.description || sessionToDelete.id}" deleted successfully`);
} catch (err) {
console.error('Failed to delete session:', err);
toast.error('Failed to delete session. Please try again later.');
} finally {
setIsDeleting(false);
setSessionToDelete(null);
}
};

// Render a session item
const SessionItem = React.memo(function SessionItem({ session }: { session: Session }) {
// Prevent event bubbling when clicking delete button
const handleDeleteClick = (e: React.MouseEvent) => {
e.stopPropagation();
setSessionToDelete(session);
};

return (
<Card
onClick={() => onSelectSession(session.id)}
Expand Down Expand Up @@ -199,6 +233,14 @@ const SessionListView: React.FC<SessionListViewProps> = ({ setView, onSelectSess
)}
</div>
</div>
<button
onClick={handleDeleteClick}
className="text-textSubtle hover:text-red-500 p-1 rounded-full hover:bg-red-100 dark:hover:bg-red-900/30 transition-colors"
title="Delete session"
aria-label="Delete session"
>
<Trash2 className="w-4 h-4" />
</button>
<ChevronRight className="w-8 h-5 text-textSubtle" />
</div>
</div>
Expand Down Expand Up @@ -298,6 +340,57 @@ const SessionListView: React.FC<SessionListViewProps> = ({ setView, onSelectSess
</ScrollArea>
</div>
</div>

{/* Delete Confirmation Modal */}
<Modal open={sessionToDelete !== null} onOpenChange={() => !isDeleting && setSessionToDelete(null)}>
<ModalContent className="sm:max-w-md p-0 bg-bgApp dark:bg-bgApp dark:border-borderSubtle">
<div className="flex justify-center mt-4">
<Trash2 className="w-6 h-6 text-red-500" />
</div>

<div className="mt-2 px-6 text-center">
<h2 className="text-lg font-semibold text-textStandard">Delete Session</h2>
</div>

<div className="px-6 flex flex-col gap-4 mt-2">
<p className="text-sm text-center text-textSubtle">
Are you sure you want to delete the session{' '}
<span className="font-semibold">
"{sessionToDelete?.metadata.description || sessionToDelete?.id}"
</span>
? This action cannot be undone.
</p>
</div>

<div className="flex border-t mt-4 dark:border-gray-600">
<Button
type="button"
variant="ghost"
onClick={() => setSessionToDelete(null)}
disabled={isDeleting}
className="flex-1 h-[60px] text-textStandard hover:bg-gray-100 hover:dark:bg-gray-600"
>
Cancel
</Button>
<Button
type="button"
variant="ghost"
onClick={handleDeleteConfirm}
disabled={isDeleting}
className="flex-1 h-[60px] text-red-500 hover:bg-red-50 hover:dark:bg-red-900/20 border-l dark:border-gray-600"
>
{isDeleting ? (
<>
<LoaderCircle className="h-4 w-4 animate-spin mr-2" />
Deleting...
</>
) : (
'Delete'
)}
</Button>
</div>
</ModalContent>
</Modal>
</div>
);
};
Expand Down
33 changes: 33 additions & 0 deletions ui/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1059,6 +1059,39 @@ ipcMain.handle('get-binary-path', (_event, binaryName) => {
return getBinaryPath(app, binaryName);
});

ipcMain.handle('delete-session-file', async (_event, sessionId) => {
try {
// Validate sessionId format (should be a date-based ID like YYYYMMDD_HHMMSS)
if (!sessionId || typeof sessionId !== 'string' || !/^\d{8}_\d{6}$/.test(sessionId)) {
console.error(`Invalid session ID format: ${sessionId}`);
return false;
}

// Get the sessions directory path
const sessionDir = path.join(app.getPath('home'), '.local', 'share', 'goose', 'sessions');
const sessionFilePath = path.join(sessionDir, `${sessionId}.jsonl`);

// Ensure the path is within the sessions directory (security check)
const resolvedPath = path.resolve(sessionFilePath);
const resolvedSessionDir = path.resolve(sessionDir);
if (!resolvedPath.startsWith(resolvedSessionDir + path.sep)) {
console.error(`Attempted path traversal detected: ${sessionFilePath}`);
return false;
}

// Check if the file exists
await fs.access(sessionFilePath);

// Delete the file
await fs.unlink(sessionFilePath);
console.log(`Session file deleted: ${sessionFilePath}`);
return true;
} catch (error) {
console.error(`Error deleting session file for ${sessionId}:`, error);
return false;
}
});

ipcMain.handle('read-file', (_event, filePath) => {
return new Promise((resolve) => {
const cat = spawn('cat', [filePath]);
Expand Down
2 changes: 2 additions & 0 deletions ui/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type ElectronAPI = {
getBinaryPath: (binaryName: string) => Promise<string>;
readFile: (directory: string) => Promise<FileResponse>;
writeFile: (directory: string, content: string) => Promise<boolean>;
deleteSessionFile: (sessionId: string) => Promise<boolean>;
getAllowedExtensions: () => Promise<string[]>;
getPathForFile: (file: File) => string;
setMenuBarIcon: (show: boolean) => Promise<boolean>;
Expand Down Expand Up @@ -119,6 +120,7 @@ const electronAPI: ElectronAPI = {
readFile: (filePath: string) => ipcRenderer.invoke('read-file', filePath),
writeFile: (filePath: string, content: string) =>
ipcRenderer.invoke('write-file', filePath, content),
deleteSessionFile: (sessionId: string) => ipcRenderer.invoke('delete-session-file', sessionId),
getPathForFile: (file: File) => webUtils.getPathForFile(file),
getAllowedExtensions: () => ipcRenderer.invoke('get-allowed-extensions'),
setMenuBarIcon: (show: boolean) => ipcRenderer.invoke('set-menu-bar-icon', show),
Expand Down
15 changes: 15 additions & 0 deletions ui/desktop/src/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,21 @@ export interface SessionDetails {
messages: Message[];
}

/**
* Deletes a session file
* @param sessionId The ID of the session to delete
* @returns Promise that resolves when the session is deleted
*/
export async function deleteSession(sessionId: string): Promise<void> {
try {
// Use Electron IPC to delete the session file
await window.electron.deleteSessionFile(sessionId);
} catch (error) {
console.error(`Error deleting session ${sessionId}:`, error);
throw error;
}
}

/**
* Generate a session ID in the format yyyymmdd_hhmmss
*/
Expand Down