From ee9d39bc0fb94108c19eb9a30546f3e01a3cd7ba Mon Sep 17 00:00:00 2001 From: Otto Date: Tue, 17 Feb 2026 12:12:27 +0000 Subject: [PATCH] refactor(copilot): Replace legacy delete dialog with molecules/Dialog (#12136) ## Summary Updates the session delete confirmation in CoPilot to use the new `Dialog` component from `molecules/Dialog` instead of the legacy `DeleteConfirmDialog`. ## Changes - **ChatSidebar**: Use Dialog component for delete confirmation (desktop) - **CopilotPage**: Use Dialog component for delete confirmation (mobile) ## Behavior - Dialog stays **open** during deletion with loading state on button - Cancel button **disabled** while delete is in progress - Delete button shows **loading spinner** during deletion - Dialog only closes on successful delete or when cancel is clicked (if not deleting) ## Screenshots *Dialog uses the same styling as other molecules/Dialog instances in the app* ## Requested by @0ubbe

Greptile Summary

Replaces the legacy `DeleteConfirmDialog` component with the new `molecules/Dialog` component for session delete confirmations in both desktop (ChatSidebar) and mobile (CopilotPage) views. The new implementation maintains the same behavior: dialog stays open during deletion with a loading state on the delete button and disabled cancel button, closing only on successful deletion or cancel click.

Confidence Score: 5/5

- This PR is safe to merge with minimal risk - This is a straightforward component replacement that maintains the same behavior and UX. The Dialog component API is properly used with controlled state, the loading states are correctly implemented, and both mobile and desktop views are handled consistently. The changes are well-tested patterns used elsewhere in the codebase. - No files require special attention

Flowchart

```mermaid flowchart TD A[User clicks delete button] --> B{isMobile?} B -->|Yes| C[CopilotPage Dialog] B -->|No| D[ChatSidebar Dialog] C --> E[Set sessionToDelete state] D --> E E --> F[Dialog opens with controlled.isOpen] F --> G{User action?} G -->|Cancel| H{isDeleting?} H -->|No| I[handleCancelDelete: setSessionToDelete null] H -->|Yes| J[Cancel button disabled] G -->|Confirm Delete| K[handleConfirmDelete called] K --> L[deleteSession mutation] L --> M[isDeleting = true] M --> N[Button shows loading spinner] M --> O[Cancel button disabled] L --> P{Mutation result?} P -->|Success| Q[Invalidate sessions query] Q --> R[Clear sessionId if current] R --> S[setSessionToDelete null] S --> T[Dialog closes] P -->|Error| U[Show toast error] U --> V[setSessionToDelete null] V --> W[Dialog closes] ```
Last reviewed commit: 275950c --------- Co-authored-by: Lluis Agusti Co-authored-by: Ubbe --- .../app/(platform)/copilot/CopilotPage.tsx | 67 +++++++++++++------ .../ChatContainer/ChatContainer.tsx | 4 ++ .../ChatMessagesContainer.tsx | 3 + .../components/ChatSidebar/ChatSidebar.tsx | 62 +++++++++++------ .../DeleteChatDialog/DeleteChatDialog.tsx | 57 ++++++++++++++++ .../components/MobileHeader/MobileHeader.tsx | 24 +------ .../app/(platform)/copilot/useCopilotPage.ts | 6 +- .../Dialog/components/BaseFooter.tsx | 2 +- .../frontend/src/tests/pages/library.page.ts | 19 ++++-- 9 files changed, 171 insertions(+), 73 deletions(-) create mode 100644 autogpt_platform/frontend/src/app/(platform)/copilot/components/DeleteChatDialog/DeleteChatDialog.tsx diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/CopilotPage.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/CopilotPage.tsx index 35b34890ce..29057d85e2 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/CopilotPage.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/CopilotPage.tsx @@ -1,10 +1,16 @@ "use client"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/molecules/DropdownMenu/DropdownMenu"; import { SidebarProvider } from "@/components/ui/sidebar"; -// TODO: Replace with modern Dialog component when available -import DeleteConfirmDialog from "@/components/__legacy__/delete-confirm-dialog"; +import { DotsThree } from "@phosphor-icons/react"; import { ChatContainer } from "./components/ChatContainer/ChatContainer"; import { ChatSidebar } from "./components/ChatSidebar/ChatSidebar"; +import { DeleteChatDialog } from "./components/DeleteChatDialog/DeleteChatDialog"; import { MobileDrawer } from "./components/MobileDrawer/MobileDrawer"; import { MobileHeader } from "./components/MobileHeader/MobileHeader"; import { ScaleLoader } from "./components/ScaleLoader/ScaleLoader"; @@ -56,19 +62,7 @@ export function CopilotPage() { > {!isMobile && }
- {isMobile && ( - { - const session = sessions.find((s) => s.id === sessionId); - if (session) { - handleDeleteClick(session.id, session.title); - } - }} - /> - )} + {isMobile && }
+ + + + + + { + const session = sessions.find( + (s) => s.id === sessionId, + ); + if (session) { + handleDeleteClick(session.id, session.title); + } + }} + disabled={isDeleting} + className="text-red-600 focus:bg-red-50 focus:text-red-600" + > + Delete chat + + + +
+ ) : undefined + } />
@@ -97,12 +123,11 @@ export function CopilotPage() { )} {/* 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/ChatContainer/ChatContainer.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatContainer/ChatContainer.tsx index 5074741095..5c4dd0dfe7 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatContainer/ChatContainer.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatContainer/ChatContainer.tsx @@ -2,6 +2,7 @@ import { ChatInput } from "@/app/(platform)/copilot/components/ChatInput/ChatInput"; import { UIDataTypes, UIMessage, UITools } from "ai"; import { LayoutGroup, motion } from "framer-motion"; +import { ReactNode } from "react"; import { ChatMessagesContainer } from "../ChatMessagesContainer/ChatMessagesContainer"; import { CopilotChatActionsProvider } from "../CopilotChatActionsProvider/CopilotChatActionsProvider"; import { EmptySession } from "../EmptySession/EmptySession"; @@ -16,6 +17,7 @@ export interface ChatContainerProps { onCreateSession: () => void | Promise; onSend: (message: string) => void | Promise; onStop: () => void; + headerSlot?: ReactNode; } export const ChatContainer = ({ messages, @@ -27,6 +29,7 @@ export const ChatContainer = ({ onCreateSession, onSend, onStop, + headerSlot, }: ChatContainerProps) => { const inputLayoutId = "copilot-2-chat-input"; @@ -41,6 +44,7 @@ export const ChatContainer = ({ status={status} error={error} isLoading={isLoadingSession} + headerSlot={headerSlot} /> { const [thinkingPhrase, setThinkingPhrase] = useState(getRandomPhrase); const lastToastTimeRef = useRef(0); @@ -165,6 +167,7 @@ export const ChatMessagesContainer = ({ return ( + {headerSlot} {isLoading && messages.length === 0 && (
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 8e785dd9d3..8fc9c3f438 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 @@ -7,9 +7,13 @@ import { import { Button } from "@/components/atoms/Button/Button"; import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner"; import { Text } from "@/components/atoms/Text/Text"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/molecules/DropdownMenu/DropdownMenu"; 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, @@ -19,11 +23,12 @@ import { useSidebar, } from "@/components/ui/sidebar"; import { cn } from "@/lib/utils"; -import { PlusCircleIcon, PlusIcon, TrashIcon } from "@phosphor-icons/react"; +import { DotsThree, PlusCircleIcon, PlusIcon } from "@phosphor-icons/react"; import { useQueryClient } from "@tanstack/react-query"; import { motion } from "framer-motion"; -import { useState } from "react"; import { parseAsString, useQueryState } from "nuqs"; +import { useState } from "react"; +import { DeleteChatDialog } from "../DeleteChatDialog/DeleteChatDialog"; export function ChatSidebar() { const { state } = useSidebar(); @@ -92,6 +97,12 @@ export function ChatSidebar() { } } + function handleCancelDelete() { + if (!isDeleting) { + setSessionToDelete(null); + } + } + function formatDate(dateString: string) { const date = new Date(dateString); const now = new Date(); @@ -220,16 +231,28 @@ export function ChatSidebar() {
- + + + + + + + handleDeleteClick(e, session.id, session.title) + } + disabled={isDeleting} + className="text-red-600 focus:bg-red-50 focus:text-red-600" + > + Delete chat + + + )) )} @@ -257,12 +280,11 @@ export function ChatSidebar() { )} - !open && setSessionToDelete(null)} - onDoDelete={handleConfirmDelete} + ); diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/DeleteChatDialog/DeleteChatDialog.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/DeleteChatDialog/DeleteChatDialog.tsx new file mode 100644 index 0000000000..d94625c4e3 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/DeleteChatDialog/DeleteChatDialog.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { Button } from "@/components/atoms/Button/Button"; +import { Text } from "@/components/atoms/Text/Text"; +import { Dialog } from "@/components/molecules/Dialog/Dialog"; + +interface Props { + session: { id: string; title: string | null | undefined } | null; + isDeleting: boolean; + onConfirm: () => void; + onCancel: () => void; +} + +export function DeleteChatDialog({ + session, + isDeleting, + onConfirm, + onCancel, +}: Props) { + return ( + { + if (!open && !isDeleting) { + onCancel(); + } + }, + }} + onClose={isDeleting ? undefined : onCancel} + > + + + Are you sure you want to delete{" "} + + "{session?.title || "Untitled chat"}" + + ? This action cannot be undone. + + + + + + + + ); +} 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 b4b7636c81..bed9989244 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,20 +1,12 @@ import { Button } from "@/components/atoms/Button/Button"; import { NAVBAR_HEIGHT_PX } from "@/lib/constants"; -import { ListIcon, TrashIcon } from "@phosphor-icons/react"; +import { ListIcon } from "@phosphor-icons/react"; interface Props { onOpenDrawer: () => void; - showDelete?: boolean; - isDeleting?: boolean; - onDelete?: () => void; } -export function MobileHeader({ - onOpenDrawer, - showDelete, - isDeleting, - onDelete, -}: Props) { +export function MobileHeader({ onOpenDrawer }: 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 444e745ec6..a0f0a2b7fd 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts @@ -192,8 +192,10 @@ export function useCopilotPage() { }, [sessionToDelete, deleteSessionMutation]); const handleCancelDelete = useCallback(() => { - setSessionToDelete(null); - }, []); + if (!isDeleting) { + setSessionToDelete(null); + } + }, [isDeleting]); return { sessionId, diff --git a/autogpt_platform/frontend/src/components/molecules/Dialog/components/BaseFooter.tsx b/autogpt_platform/frontend/src/components/molecules/Dialog/components/BaseFooter.tsx index 3ed1157b6c..1951525ccd 100644 --- a/autogpt_platform/frontend/src/components/molecules/Dialog/components/BaseFooter.tsx +++ b/autogpt_platform/frontend/src/components/molecules/Dialog/components/BaseFooter.tsx @@ -25,7 +25,7 @@ export function BaseFooter({ ) : (
{children} diff --git a/autogpt_platform/frontend/src/tests/pages/library.page.ts b/autogpt_platform/frontend/src/tests/pages/library.page.ts index 03e98598b4..17a265d590 100644 --- a/autogpt_platform/frontend/src/tests/pages/library.page.ts +++ b/autogpt_platform/frontend/src/tests/pages/library.page.ts @@ -465,9 +465,13 @@ export async function navigateToAgentByName( export async function clickRunButton(page: Page): Promise { const { getId } = getSelectors(page); - // Wait for page to stabilize and buttons to render - // The NewAgentLibraryView shows either "Setup your task" (empty state) - // or "New task" (with items) button + // Wait for sidebar loading to complete before detecting buttons. + // During sidebar loading, the "New task" button appears transiently + // even for agents with no items, then switches to "Setup your task" + // once loading finishes. Waiting for network idle ensures the page + // has settled into its final state. + await page.waitForLoadState("networkidle"); + const setupTaskButton = page.getByRole("button", { name: /Setup your task/i, }); @@ -475,8 +479,7 @@ export async function clickRunButton(page: Page): Promise { const runButton = getId("agent-run-button"); const runAgainButton = getId("run-again-button"); - // Use Promise.race with waitFor to wait for any of the buttons to appear - // This handles the async rendering in CI environments + // Wait for any of the buttons to appear try { await Promise.race([ setupTaskButton.waitFor({ state: "visible", timeout: 15000 }), @@ -490,7 +493,7 @@ export async function clickRunButton(page: Page): Promise { ); } - // Now check which button is visible and click it + // Check which button is visible and click it if (await setupTaskButton.isVisible()) { await setupTaskButton.click(); const startTaskButton = page @@ -534,7 +537,9 @@ export async function runAgent(page: Page): Promise { export async function waitForAgentPageLoad(page: Page): Promise { await page.waitForURL(/.*\/library\/agents\/[^/]+/); - await page.getByTestId("Run actions").isVisible({ timeout: 10000 }); + // Wait for sidebar data to finish loading so the page settles + // into its final state (empty view vs sidebar view) + await page.waitForLoadState("networkidle"); } export async function getAgentName(page: Page): Promise {