From a086118e0d8f3a712c4569a50aa0c52daf306fc7 Mon Sep 17 00:00:00 2001 From: Otto Date: Sat, 14 Feb 2026 12:39:46 +0000 Subject: [PATCH] feat(chat): add delete chat session endpoint and UI Adds the ability to delete chat sessions from the CoPilot interface: Backend: - Add DELETE /api/chat/sessions/{session_id} endpoint - Returns 204 on success, 404 if not found or not owned Frontend: - Add delete button (trash icon) on hover for each chat session - Add confirmation dialog before deletion - Refresh session list after successful delete - Clear current session if deleted Closes: SECRT-1928 --- .../backend/api/features/chat/routes.py | 37 ++ .../components/ChatSidebar/ChatSidebar.tsx | 316 +++++++++++------- .../frontend/src/app/api/openapi.json | 34 ++ notes/plan-SECRT-1928-delete-chat-sessions.md | 235 +++++++++++++ notes/plan-SECRT-1959-graph-edge-desync.md | 221 ++++++++++++ 5 files changed, 721 insertions(+), 122 deletions(-) create mode 100644 notes/plan-SECRT-1928-delete-chat-sessions.md create mode 100644 notes/plan-SECRT-1959-graph-edge-desync.md diff --git a/autogpt_platform/backend/backend/api/features/chat/routes.py b/autogpt_platform/backend/backend/api/features/chat/routes.py index aa565ca891..aa95373257 100644 --- a/autogpt_platform/backend/backend/api/features/chat/routes.py +++ b/autogpt_platform/backend/backend/api/features/chat/routes.py @@ -23,6 +23,7 @@ from .model import ( ChatSession, append_and_save_message, create_chat_session, + delete_chat_session, get_chat_session, get_user_sessions, ) @@ -211,6 +212,42 @@ async def create_session( ) +@router.delete( + "/sessions/{session_id}", + dependencies=[Security(auth.requires_user)], + status_code=204, +) +async def delete_session( + session_id: str, + user_id: Annotated[str, Security(auth.get_user_id)], +) -> Response: + """ + Delete a chat session. + + Permanently removes a chat session and all its messages. + Only the owner can delete their sessions. + + Args: + session_id: The session ID to delete. + user_id: The authenticated user's ID. + + Returns: + 204 No Content on success. + + Raises: + HTTPException: 404 if session not found or not owned by user. + """ + deleted = await delete_chat_session(session_id, user_id) + + if not deleted: + raise HTTPException( + status_code=404, + detail=f"Session {session_id} not found or access denied", + ) + + return Response(status_code=204) + + @router.get( "/sessions/{session_id}", ) diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatSidebar/ChatSidebar.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatSidebar/ChatSidebar.tsx index 6b7398b4ba..2cfad63f0f 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatSidebar/ChatSidebar.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatSidebar/ChatSidebar.tsx @@ -1,8 +1,13 @@ "use client"; -import { useGetV2ListSessions } from "@/app/api/__generated__/endpoints/chat/chat"; +import { + getGetV2ListSessionsQueryKey, + useDeleteV2DeleteSession, + useGetV2ListSessions, +} from "@/app/api/__generated__/endpoints/chat/chat"; import { Button } from "@/components/atoms/Button/Button"; import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner"; import { Text } from "@/components/atoms/Text/Text"; +import DeleteConfirmDialog from "@/components/__legacy__/delete-confirm-dialog"; import { Sidebar, SidebarContent, @@ -12,18 +17,47 @@ import { useSidebar, } from "@/components/ui/sidebar"; import { cn } from "@/lib/utils"; -import { PlusCircleIcon, PlusIcon } from "@phosphor-icons/react"; +import { PlusCircleIcon, PlusIcon, TrashIcon } from "@phosphor-icons/react"; +import { useQueryClient } from "@tanstack/react-query"; import { motion } from "framer-motion"; +import { useState } from "react"; import { parseAsString, useQueryState } from "nuqs"; export function ChatSidebar() { const { state } = useSidebar(); const isCollapsed = state === "collapsed"; const [sessionId, setSessionId] = useQueryState("sessionId", parseAsString); + const [sessionToDelete, setSessionToDelete] = useState<{ + id: string; + title: string | null; + } | null>(null); + + const queryClient = useQueryClient(); const { data: sessionsResponse, isLoading: isLoadingSessions } = useGetV2ListSessions({ limit: 50 }); + const { mutate: deleteSession, isPending: isDeleting } = + useDeleteV2DeleteSession({ + mutation: { + onSuccess: () => { + // Invalidate sessions list to refetch + queryClient.invalidateQueries({ + queryKey: getGetV2ListSessionsQueryKey(), + }); + // If we deleted the current session, clear selection + if (sessionToDelete?.id === sessionId) { + setSessionId(null); + } + setSessionToDelete(null); + }, + onError: (error) => { + console.error("Failed to delete session:", error); + setSessionToDelete(null); + }, + }, + }); + const sessions = sessionsResponse?.status === 200 ? sessionsResponse.data.sessions : []; @@ -35,6 +69,21 @@ export function ChatSidebar() { setSessionId(id); } + function handleDeleteClick( + e: React.MouseEvent, + id: string, + title: string | null, + ) { + e.stopPropagation(); // Prevent session selection + setSessionToDelete({ id, title }); + } + + function handleConfirmDelete() { + if (sessionToDelete) { + deleteSession({ sessionId: sessionToDelete.id }); + } + } + function formatDate(dateString: string) { const date = new Date(dateString); const now = new Date(); @@ -61,128 +110,151 @@ export function ChatSidebar() { } return ( - - {isCollapsed && ( - - -
- - -
-
-
- )} - - {!isCollapsed && ( - - - Your chats - -
- -
-
- )} - - {!isCollapsed && ( - - {isLoadingSessions ? ( -
- -
- ) : sessions.length === 0 ? ( -

- No conversations yet -

- ) : ( - sessions.map((session) => ( - - )) + <> + + {isCollapsed && ( + - )} -
- {!isCollapsed && sessionId && ( - - - - - - )} -
+
+ + +
+ + + )} + + {!isCollapsed && ( + + + Your chats + +
+ +
+
+ )} + + {!isCollapsed && ( + + {isLoadingSessions ? ( +
+ +
+ ) : sessions.length === 0 ? ( +

+ No conversations yet +

+ ) : ( + sessions.map((session) => ( +
+ + +
+ )) + )} +
+ )} +
+ {!isCollapsed && sessionId && ( + + + + + + )} + + + !open && setSessionToDelete(null)} + onDoDelete={handleConfirmDelete} + /> + ); } diff --git a/autogpt_platform/frontend/src/app/api/openapi.json b/autogpt_platform/frontend/src/app/api/openapi.json index 63a8a856b9..02d55b4b90 100644 --- a/autogpt_platform/frontend/src/app/api/openapi.json +++ b/autogpt_platform/frontend/src/app/api/openapi.json @@ -1188,6 +1188,40 @@ } } } + }, + "delete": { + "tags": ["v2", "chat", "chat"], + "summary": "Delete Session", + "description": "Delete a chat session.\n\nPermanently removes a chat session and all its messages.\nOnly the owner can delete their sessions.\n\nArgs:\n session_id: The session ID to delete.\n user_id: The authenticated user's ID.\n\nReturns:\n 204 No Content on success.", + "operationId": "deleteV2DeleteSession", + "security": [{ "HTTPBearerJWT": [] }], + "parameters": [ + { + "name": "session_id", + "in": "path", + "required": true, + "schema": { "type": "string", "title": "Session Id" } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "401": { + "$ref": "#/components/responses/HTTP401NotAuthenticatedError" + }, + "404": { + "description": "Session not found or access denied" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/HTTPValidationError" } + } + } + } + } } }, "/api/chat/sessions/{session_id}/assign-user": { diff --git a/notes/plan-SECRT-1928-delete-chat-sessions.md b/notes/plan-SECRT-1928-delete-chat-sessions.md new file mode 100644 index 0000000000..fb23f008e3 --- /dev/null +++ b/notes/plan-SECRT-1928-delete-chat-sessions.md @@ -0,0 +1,235 @@ +# Implementation Plan: SECRT-1928 - Delete Chat Sessions + +**Ticket:** [SECRT-1928](https://linear.app/autogpt/issue/SECRT-1928) +**Author:** Otto +**Date:** 2026-02-14 + +## Summary + +Add the ability for users to delete chat sessions from the CoPilot interface. The backend logic already exists (`delete_chat_session` in `model.py` and `db.py`), it just needs a route and frontend UI. + +## Current State + +### Backend (already exists) +- `backend/api/features/chat/db.py:delete_chat_session()` - DB deletion with user ownership validation +- `backend/api/features/chat/model.py:delete_chat_session()` - Handles cache cleanup and lock removal +- **Missing:** No DELETE route in `routes.py` + +### Frontend +- `ChatSidebar.tsx` displays session list with no delete option +- `MobileDrawer.tsx` also needs delete option +- **Missing:** Delete button, confirmation dialog, API call + +## Implementation + +### Phase 1: Backend Route (15 min) + +**File:** `autogpt_platform/backend/backend/api/features/chat/routes.py` + +Add after line ~200 (after `create_session`): + +```python +@router.delete( + "/sessions/{session_id}", + dependencies=[Security(auth.requires_user)], + status_code=204, +) +async def delete_session( + session_id: str, + user_id: Annotated[str, Security(auth.get_user_id)], +) -> Response: + """ + Delete a chat session. + + Permanently removes a chat session and all its messages. + Only the owner can delete their sessions. + + Args: + session_id: The session ID to delete. + user_id: The authenticated user's ID. + + Returns: + 204 No Content on success. + + Raises: + 404: Session not found or not owned by user. + """ + from .model import delete_chat_session + + deleted = await delete_chat_session(session_id, user_id) + + if not deleted: + raise HTTPException( + status_code=404, + detail=f"Session {session_id} not found or access denied" + ) + + return Response(status_code=204) +``` + +**Add import at top:** +```python +from fastapi import Response # add to existing imports +``` + +### Phase 2: Frontend API Hook (auto-generated) + +After adding the backend route, run the OpenAPI generator to create the hook: +```bash +cd autogpt_platform/frontend +pnpm generate:api +``` + +This will generate `useDeleteV2Session` or similar. + +### Phase 3: Frontend UI (30 min) + +**File:** `autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatSidebar/ChatSidebar.tsx` + +1. Add imports: +```tsx +import { TrashIcon } from "@phosphor-icons/react"; +import { useDeleteV2Session } from "@/app/api/__generated__/endpoints/chat/chat"; +import { useQueryClient } from "@tanstack/react-query"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +``` + +2. Add state and mutation hook inside component: +```tsx +const [sessionToDelete, setSessionToDelete] = useState(null); +const queryClient = useQueryClient(); + +const { mutate: deleteSession, isPending: isDeleting } = useDeleteV2Session({ + mutation: { + onSuccess: () => { + // Invalidate sessions list to refetch + queryClient.invalidateQueries({ queryKey: ['v2', 'sessions'] }); + // If we deleted the current session, clear selection + if (sessionToDelete === sessionId) { + setSessionId(null); + } + setSessionToDelete(null); + }, + onError: (error) => { + console.error("Failed to delete session:", error); + setSessionToDelete(null); + }, + }, +}); + +function handleDeleteClick(e: React.MouseEvent, id: string) { + e.stopPropagation(); // Prevent session selection + setSessionToDelete(id); +} + +function handleConfirmDelete() { + if (sessionToDelete) { + deleteSession({ sessionId: sessionToDelete }); + } +} +``` + +3. Add delete button to each session item (inside the session button, after the date): +```tsx + +``` + +4. Add confirmation dialog before the closing ``: +```tsx + !open && setSessionToDelete(null)}> + + + Delete this chat? + + This will permanently delete this conversation and all its messages. This action cannot be undone. + + + + Cancel + + {isDeleting ? "Deleting..." : "Delete"} + + + + +``` + +5. Update session button to have `group` class for hover effects: +```tsx +