From f74b44727aeb3dbb3d71e948771cf2a1a84a102a Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Fri, 20 Mar 2026 22:05:02 -0300 Subject: [PATCH] fix(web): allow deleting non-empty projects from the warning toast --- apps/web/src/components/Sidebar.tsx | 107 +++++++++++++++++++-------- apps/web/src/components/ui/toast.tsx | 41 +++++++++- 2 files changed, 112 insertions(+), 36 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 9ff741897c..ad4ff8af6f 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -752,9 +752,7 @@ export default function Sidebar() { "This permanently clears conversation history for this thread.", ].join("\n"), ); - if (!confirmed) { - return; - } + if (!confirmed) return; } await deleteThread(threadId); }, @@ -821,6 +819,37 @@ export default function Sidebar() { ], ); + const removeProject = useCallback( + async (projectId: ProjectId): Promise => { + const api = readNativeApi(); + if (!api) return; + const project = projects.find((entry) => entry.id === projectId); + if (!project) return; + + try { + const projectDraftThread = getDraftThreadByProjectId(projectId); + if (projectDraftThread) { + clearComposerDraftForThread(projectDraftThread.threadId); + } + clearProjectDraftThreadId(projectId); + await api.orchestration.dispatchCommand({ + type: "project.delete", + commandId: newCommandId(), + projectId, + }); + } catch (error) { + const message = error instanceof Error ? error.message : "Unknown error removing project."; + console.error("Failed to remove project", { projectId, error }); + toastManager.add({ + type: "error", + title: `Failed to remove "${project.name}"`, + description: message, + }); + } + }, + [clearComposerDraftForThread, clearProjectDraftThreadId, getDraftThreadByProjectId, projects], + ); + const handleThreadClick = useCallback( (event: MouseEvent, threadId: ThreadId, orderedProjectThreadIds: readonly ThreadId[]) => { const isMac = isMacPlatform(navigator.platform); @@ -874,45 +903,59 @@ export default function Sidebar() { const projectThreads = threads.filter((thread) => thread.projectId === projectId); if (projectThreads.length > 0) { - toastManager.add({ + const warningToastId = toastManager.add({ type: "warning", title: "Project is not empty", description: "Delete all threads in this project before removing it.", + data: { + actionLayout: "stacked-end", + actionVariant: "destructive", + }, + actionProps: { + children: "Delete anyway", + onClick: () => { + void (async () => { + toastManager.close(warningToastId); + await new Promise((resolve) => { + window.setTimeout(resolve, 180); + }); + const confirmed = await api.dialogs.confirm( + [ + `Remove project "${project.name}" and delete its ${projectThreads.length} thread${ + projectThreads.length === 1 ? "" : "s" + }?`, + "This will permanently clear conversation history for those threads.", + "This action cannot be undone.", + ].join("\n"), + ); + if (!confirmed) return; + + const deletedThreadIds = new Set( + projectThreads.map((thread) => thread.id), + ); + for (const thread of projectThreads) { + await deleteThread(thread.id, { deletedThreadIds }); + } + await removeProject(projectId); + })().catch((error) => { + toastManager.add({ + type: "error", + title: `Failed to remove "${project.name}"`, + description: + error instanceof Error ? error.message : "Unknown error removing project.", + }); + }); + }, + }, }); return; } const confirmed = await api.dialogs.confirm(`Remove project "${project.name}"?`); if (!confirmed) return; - - try { - const projectDraftThread = getDraftThreadByProjectId(projectId); - if (projectDraftThread) { - clearComposerDraftForThread(projectDraftThread.threadId); - } - clearProjectDraftThreadId(projectId); - await api.orchestration.dispatchCommand({ - type: "project.delete", - commandId: newCommandId(), - projectId, - }); - } catch (error) { - const message = error instanceof Error ? error.message : "Unknown error removing project."; - console.error("Failed to remove project", { projectId, error }); - toastManager.add({ - type: "error", - title: `Failed to remove "${project.name}"`, - description: message, - }); - } + await removeProject(projectId); }, - [ - clearComposerDraftForThread, - clearProjectDraftThreadId, - getDraftThreadByProjectId, - projects, - threads, - ], + [deleteThread, projects, removeProject, threads], ); const projectDnDSensors = useSensors( diff --git a/apps/web/src/components/ui/toast.tsx b/apps/web/src/components/ui/toast.tsx index 768a083e2e..af8b5e2d08 100644 --- a/apps/web/src/components/ui/toast.tsx +++ b/apps/web/src/components/ui/toast.tsx @@ -20,6 +20,15 @@ type ThreadToastData = { threadId?: ThreadId | null; tooltipStyle?: boolean; dismissAfterVisibleMs?: number; + actionLayout?: "inline" | "stacked-end"; + actionVariant?: + | "default" + | "destructive" + | "destructive-outline" + | "ghost" + | "link" + | "outline" + | "secondary"; }; const toastManager = Toast.createToastManager(); @@ -196,6 +205,9 @@ function Toasts({ position = "top-right" }: { position: ToastPosition }) { visibleIndex, visibleToastLayout.items.length, ); + const stackedActionLayout = + toast.actionProps !== undefined && toast.data?.actionLayout === "stacked-end"; + const actionVariant = toast.data?.actionVariant ?? "default"; return ( {toast.actionProps && ( {toast.actionProps.children} @@ -333,6 +352,9 @@ function AnchoredToasts() { const Icon = toast.type ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS] : null; const tooltipStyle = toast.data?.tooltipStyle ?? false; const positionerProps = toast.positionerProps; + const stackedActionLayout = + toast.actionProps !== undefined && toast.data?.actionLayout === "stacked-end"; + const actionVariant = toast.data?.actionVariant ?? "default"; if (!positionerProps?.anchor) { return null; @@ -361,7 +383,14 @@ function AnchoredToasts() { ) : ( - +
{Icon && (
{toast.actionProps && ( {toast.actionProps.children}