diff --git a/.github/workflows/platform-backend-ci.yml b/.github/workflows/platform-backend-ci.yml index 1f0c6da3dd..22d1e91ead 100644 --- a/.github/workflows/platform-backend-ci.yml +++ b/.github/workflows/platform-backend-ci.yml @@ -41,13 +41,18 @@ jobs: ports: - 6379:6379 rabbitmq: - image: rabbitmq:3.12-management + image: rabbitmq:4.1.4 ports: - 5672:5672 - - 15672:15672 env: RABBITMQ_DEFAULT_USER: ${{ env.RABBITMQ_DEFAULT_USER }} RABBITMQ_DEFAULT_PASS: ${{ env.RABBITMQ_DEFAULT_PASS }} + options: >- + --health-cmd "rabbitmq-diagnostics -q ping" + --health-interval 30s + --health-timeout 10s + --health-retries 5 + --health-start-period 10s clamav: image: clamav/clamav-debian:latest ports: diff --git a/.github/workflows/platform-frontend-ci.yml b/.github/workflows/platform-frontend-ci.yml index 4bf8a2b80c..e788696f9b 100644 --- a/.github/workflows/platform-frontend-ci.yml +++ b/.github/workflows/platform-frontend-ci.yml @@ -6,10 +6,16 @@ on: paths: - ".github/workflows/platform-frontend-ci.yml" - "autogpt_platform/frontend/**" + - "autogpt_platform/backend/Dockerfile" + - "autogpt_platform/docker-compose.yml" + - "autogpt_platform/docker-compose.platform.yml" pull_request: paths: - ".github/workflows/platform-frontend-ci.yml" - "autogpt_platform/frontend/**" + - "autogpt_platform/backend/Dockerfile" + - "autogpt_platform/docker-compose.yml" + - "autogpt_platform/docker-compose.platform.yml" merge_group: workflow_dispatch: diff --git a/autogpt_platform/backend/Dockerfile b/autogpt_platform/backend/Dockerfile index 05a8d4858b..6037ed656f 100644 --- a/autogpt_platform/backend/Dockerfile +++ b/autogpt_platform/backend/Dockerfile @@ -59,12 +59,7 @@ FROM debian:13-slim AS server WORKDIR /app -ENV POETRY_HOME=/opt/poetry \ - POETRY_NO_INTERACTION=1 \ - POETRY_VIRTUALENVS_CREATE=true \ - POETRY_VIRTUALENVS_IN_PROJECT=true \ - DEBIAN_FRONTEND=noninteractive -ENV PATH=/opt/poetry/bin:$PATH +ENV DEBIAN_FRONTEND=noninteractive # Install Python, FFmpeg, ImageMagick, and CLI tools for agent use. # bubblewrap provides OS-level sandbox (whitelist-only FS + no network) @@ -81,6 +76,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ bubblewrap \ && rm -rf /var/lib/apt/lists/* +# Copy poetry (build-time only, for `poetry install --only-root` to create entry points) COPY --from=builder /usr/local/lib/python3* /usr/local/lib/python3* COPY --from=builder /usr/local/bin/poetry /usr/local/bin/poetry # Copy Node.js installation for Prisma @@ -104,11 +100,12 @@ COPY autogpt_platform/backend/poetry.lock autogpt_platform/backend/pyproject.tom # Copy backend code + docs (for Copilot docs search) COPY autogpt_platform/backend ./ COPY docs /app/docs -RUN poetry install --no-ansi --only-root +RUN POETRY_VIRTUALENVS_CREATE=true POETRY_VIRTUALENVS_IN_PROJECT=true \ + poetry install --no-ansi --only-root ENV PORT=8000 -CMD ["poetry", "run", "rest"] +CMD ["rest"] # =============================== DB MIGRATOR =============================== # diff --git a/autogpt_platform/backend/backend/api/features/chat/routes.py b/autogpt_platform/backend/backend/api/features/chat/routes.py index 0f0568a349..2fd7d29319 100644 --- a/autogpt_platform/backend/backend/api/features/chat/routes.py +++ b/autogpt_platform/backend/backend/api/features/chat/routes.py @@ -24,6 +24,7 @@ from backend.copilot.model import ( ChatSession, append_and_save_message, create_chat_session, + delete_chat_session, get_chat_session, get_user_sessions, ) @@ -212,6 +213,43 @@ async def create_session( ) +@router.delete( + "/sessions/{session_id}", + dependencies=[Security(auth.requires_user)], + status_code=204, + responses={404: {"description": "Session not found or access denied"}}, +) +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/backend/docker-compose.test.yaml b/autogpt_platform/backend/docker-compose.test.yaml index 259d52c497..5944bf37ee 100644 --- a/autogpt_platform/backend/docker-compose.test.yaml +++ b/autogpt_platform/backend/docker-compose.test.yaml @@ -53,7 +53,7 @@ services: rabbitmq: <<: *agpt-services - image: rabbitmq:management + image: rabbitmq:4.1.4 container_name: rabbitmq healthcheck: test: rabbitmq-diagnostics -q ping @@ -66,7 +66,6 @@ services: - RABBITMQ_DEFAULT_PASS=k0VMxyIJF9S35f3x2uaw5IWAl6Y536O7 ports: - "5672:5672" - - "15672:15672" clamav: image: clamav/clamav-debian:latest ports: diff --git a/autogpt_platform/docker-compose.platform.yml b/autogpt_platform/docker-compose.platform.yml index 16b7843dc0..906cbb40a7 100644 --- a/autogpt_platform/docker-compose.platform.yml +++ b/autogpt_platform/docker-compose.platform.yml @@ -75,7 +75,7 @@ services: timeout: 5s retries: 5 rabbitmq: - image: rabbitmq:management + image: rabbitmq:4.1.4 container_name: rabbitmq healthcheck: test: rabbitmq-diagnostics -q ping @@ -88,14 +88,13 @@ services: <<: *backend-env ports: - "5672:5672" - - "15672:15672" rest_server: build: context: ../ dockerfile: autogpt_platform/backend/Dockerfile target: server - command: ["python", "-m", "backend.rest"] + command: ["rest"] # points to entry in [tool.poetry.scripts] in pyproject.toml develop: watch: - path: ./ @@ -128,7 +127,7 @@ services: context: ../ dockerfile: autogpt_platform/backend/Dockerfile target: server - command: ["python", "-m", "backend.exec"] + command: ["executor"] # points to entry in [tool.poetry.scripts] in pyproject.toml develop: watch: - path: ./ @@ -198,7 +197,7 @@ services: context: ../ dockerfile: autogpt_platform/backend/Dockerfile target: server - command: ["python", "-m", "backend.ws"] + command: ["ws"] # points to entry in [tool.poetry.scripts] in pyproject.toml develop: watch: - path: ./ @@ -231,7 +230,7 @@ services: context: ../ dockerfile: autogpt_platform/backend/Dockerfile target: server - command: ["python", "-m", "backend.db"] + command: ["db"] # points to entry in [tool.poetry.scripts] in pyproject.toml develop: watch: - path: ./ @@ -260,7 +259,7 @@ services: context: ../ dockerfile: autogpt_platform/backend/Dockerfile target: server - command: ["python", "-m", "backend.scheduler"] + command: ["scheduler"] # points to entry in [tool.poetry.scripts] in pyproject.toml develop: watch: - path: ./ @@ -308,7 +307,7 @@ services: context: ../ dockerfile: autogpt_platform/backend/Dockerfile target: server - command: ["python", "-m", "backend.notification"] + command: ["notification"] # points to entry in [tool.poetry.scripts] in pyproject.toml develop: watch: - path: ./ diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/CopilotPage.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/CopilotPage.tsx index 0d403b1a79..35b34890ce 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/CopilotPage.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/CopilotPage.tsx @@ -1,6 +1,8 @@ "use client"; import { SidebarProvider } from "@/components/ui/sidebar"; +// TODO: Replace with modern Dialog component when available +import DeleteConfirmDialog from "@/components/__legacy__/delete-confirm-dialog"; import { ChatContainer } from "./components/ChatContainer/ChatContainer"; import { ChatSidebar } from "./components/ChatSidebar/ChatSidebar"; import { MobileDrawer } from "./components/MobileDrawer/MobileDrawer"; @@ -31,6 +33,12 @@ export function CopilotPage() { handleDrawerOpenChange, handleSelectSession, handleNewChat, + // Delete functionality + sessionToDelete, + isDeleting, + handleDeleteClick, + handleConfirmDelete, + handleCancelDelete, } = useCopilotPage(); if (isUserLoading || !isLoggedIn) { @@ -48,7 +56,19 @@ export function CopilotPage() { > {!isMobile && }
- {isMobile && } + {isMobile && ( + { + const session = sessions.find((s) => s.id === sessionId); + if (session) { + handleDeleteClick(session.id, session.title); + } + }} + /> + )}
)} + {/* Delete confirmation dialog - rendered at top level for proper z-index on mobile */} + {isMobile && ( + !open && handleCancelDelete()} + onDoDelete={handleConfirmDelete} + /> + )} ); } 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..8e785dd9d3 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,15 @@ "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 { toast } from "@/components/molecules/Toast/use-toast"; +// TODO: Replace with modern Dialog component when available +import DeleteConfirmDialog from "@/components/__legacy__/delete-confirm-dialog"; import { Sidebar, SidebarContent, @@ -12,18 +19,52 @@ 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 | undefined; + } | 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) => { + toast({ + title: "Failed to delete chat", + description: + error instanceof Error ? error.message : "An error occurred", + variant: "destructive", + }); + setSessionToDelete(null); + }, + }, + }); + const sessions = sessionsResponse?.status === 200 ? sessionsResponse.data.sessions : []; @@ -35,6 +76,22 @@ export function ChatSidebar() { setSessionId(id); } + function handleDeleteClick( + e: React.MouseEvent, + id: string, + title: string | null | undefined, + ) { + e.stopPropagation(); // Prevent session selection + if (isDeleting) return; // Prevent double-click during deletion + 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 +118,152 @@ 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/(platform)/copilot/components/MobileHeader/MobileHeader.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/MobileHeader/MobileHeader.tsx index e0d6161744..b4b7636c81 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/MobileHeader/MobileHeader.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/MobileHeader/MobileHeader.tsx @@ -1,22 +1,46 @@ import { Button } from "@/components/atoms/Button/Button"; import { NAVBAR_HEIGHT_PX } from "@/lib/constants"; -import { ListIcon } from "@phosphor-icons/react"; +import { ListIcon, TrashIcon } from "@phosphor-icons/react"; interface Props { onOpenDrawer: () => void; + showDelete?: boolean; + isDeleting?: boolean; + onDelete?: () => void; } -export function MobileHeader({ onOpenDrawer }: Props) { +export function MobileHeader({ + onOpenDrawer, + showDelete, + isDeleting, + onDelete, +}: Props) { return ( - + + {showDelete && onDelete && ( + + )} +
); } diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts index 28e9ba7cfb..444e745ec6 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts @@ -1,10 +1,15 @@ -import { useGetV2ListSessions } from "@/app/api/__generated__/endpoints/chat/chat"; +import { + getGetV2ListSessionsQueryKey, + useDeleteV2DeleteSession, + useGetV2ListSessions, +} from "@/app/api/__generated__/endpoints/chat/chat"; import { toast } from "@/components/molecules/Toast/use-toast"; import { useBreakpoint } from "@/lib/hooks/useBreakpoint"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { useChat } from "@ai-sdk/react"; +import { useQueryClient } from "@tanstack/react-query"; import { DefaultChatTransport } from "ai"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useChatSession } from "./useChatSession"; import { useLongRunningToolPolling } from "./hooks/useLongRunningToolPolling"; @@ -14,6 +19,11 @@ export function useCopilotPage() { const { isUserLoading, isLoggedIn } = useSupabase(); const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [pendingMessage, setPendingMessage] = useState(null); + const [sessionToDelete, setSessionToDelete] = useState<{ + id: string; + title: string | null | undefined; + } | null>(null); + const queryClient = useQueryClient(); const { sessionId, @@ -24,6 +34,30 @@ export function useCopilotPage() { isCreatingSession, } = useChatSession(); + const { mutate: deleteSessionMutation, isPending: isDeleting } = + useDeleteV2DeleteSession({ + mutation: { + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: getGetV2ListSessionsQueryKey(), + }); + if (sessionToDelete?.id === sessionId) { + setSessionId(null); + } + setSessionToDelete(null); + }, + onError: (error) => { + toast({ + title: "Failed to delete chat", + description: + error instanceof Error ? error.message : "An error occurred", + variant: "destructive", + }); + setSessionToDelete(null); + }, + }, + }); + const breakpoint = useBreakpoint(); const isMobile = breakpoint === "base" || breakpoint === "sm" || breakpoint === "md"; @@ -143,6 +177,24 @@ export function useCopilotPage() { if (isMobile) setIsDrawerOpen(false); } + const handleDeleteClick = useCallback( + (id: string, title: string | null | undefined) => { + if (isDeleting) return; + setSessionToDelete({ id, title }); + }, + [isDeleting], + ); + + const handleConfirmDelete = useCallback(() => { + if (sessionToDelete) { + deleteSessionMutation({ sessionId: sessionToDelete.id }); + } + }, [sessionToDelete, deleteSessionMutation]); + + const handleCancelDelete = useCallback(() => { + setSessionToDelete(null); + }, []); + return { sessionId, messages, @@ -165,5 +217,11 @@ export function useCopilotPage() { handleDrawerOpenChange, handleSelectSession, handleNewChat, + // Delete functionality + sessionToDelete, + isDeleting, + handleDeleteClick, + handleConfirmDelete, + handleCancelDelete, }; } diff --git a/autogpt_platform/frontend/src/app/api/openapi.json b/autogpt_platform/frontend/src/app/api/openapi.json index 63a8a856b9..feabc9b51d 100644 --- a/autogpt_platform/frontend/src/app/api/openapi.json +++ b/autogpt_platform/frontend/src/app/api/openapi.json @@ -1151,6 +1151,36 @@ } }, "/api/chat/sessions/{session_id}": { + "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.\n\nRaises:\n HTTPException: 404 if session not found or not owned by user.", + "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" } + } + } + } + } + }, "get": { "tags": ["v2", "chat", "chat"], "summary": "Get Session", diff --git a/autogpt_platform/frontend/src/components/__legacy__/ui/dialog.tsx b/autogpt_platform/frontend/src/components/__legacy__/ui/dialog.tsx index 4ce998b6f6..10af4fa3c8 100644 --- a/autogpt_platform/frontend/src/components/__legacy__/ui/dialog.tsx +++ b/autogpt_platform/frontend/src/components/__legacy__/ui/dialog.tsx @@ -115,7 +115,7 @@ const DialogFooter = ({ }: React.HTMLAttributes) => (