feat: add confirmation dialog before archiving tasks (#621)

This commit is contained in:
João Salavisa
2026-04-17 18:27:10 +01:00
committed by GitHub
parent 78d783237b
commit f440007efc
5 changed files with 183 additions and 50 deletions
@@ -0,0 +1,62 @@
"use client";
import { IconLoader } from "@tabler/icons-react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@kandev/ui/alert-dialog";
type ArchiveConfirmDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
taskTitle: string;
isArchiving?: boolean;
onConfirm: () => void;
};
export function ArchiveConfirmDialog({
open,
onOpenChange,
taskTitle,
isArchiving,
onConfirm,
}: ArchiveConfirmDialogProps) {
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent onClick={(e) => e.stopPropagation()}>
<AlertDialogHeader>
<AlertDialogTitle>Archive task?</AlertDialogTitle>
<AlertDialogDescription asChild>
<div>
<p>Are you sure you want to archive &quot;{taskTitle}&quot;?</p>
<p className="mt-2">
This will delete the task&apos;s worktree and stop any running agent sessions.
</p>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="cursor-pointer">Cancel</AlertDialogCancel>
<AlertDialogAction
disabled={isArchiving}
className="cursor-pointer"
onClick={(e) => {
e.preventDefault();
if (isArchiving) return;
onConfirm();
}}
>
{isArchiving ? <IconLoader className="mr-2 h-4 w-4 animate-spin" /> : null}
Archive
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
@@ -7,13 +7,14 @@ import { Button } from "@kandev/ui/button";
import { TaskSwitcher } from "../task-switcher";
import { WorkspaceSwitcher } from "../workspace-switcher";
import { TaskCreateDialog } from "@/components/task-create-dialog";
import { ArchiveConfirmDialog } from "../archive-confirm-dialog";
import { useAppStore, useAppStoreApi } from "@/components/state-provider";
import { replaceTaskUrl } from "@/lib/links";
import { fetchWorkflowSnapshot, listWorkflows } from "@/lib/api";
import { launchSession } from "@/lib/services/session-launch-service";
import { buildPrepareRequest } from "@/lib/services/session-launch-helpers";
import { useTasks } from "@/hooks/use-tasks";
import { useTaskActions } from "@/hooks/use-task-actions";
import { useTaskActions, useArchiveAndSwitchTask } from "@/hooks/use-task-actions";
import { useTaskRemoval } from "@/hooks/use-task-removal";
import { getSessionInfoForTask } from "@/lib/utils/session-info";
import type {
@@ -273,7 +274,8 @@ function useSheetActions(workspaceId: string | null, onOpenChange: (open: boolea
const setActiveSession = useAppStore((state) => state.setActiveSession);
const store = useAppStoreApi();
const [deletingTaskId, setDeletingTaskId] = useState<string | null>(null);
const { deleteTaskById, archiveTaskById } = useTaskActions();
const { deleteTaskById } = useTaskActions();
const archiveAndSwitch = useArchiveAndSwitchTask();
const { removeTaskFromBoard, loadTaskSessionsForTask } = useTaskRemoval({ store });
const handleSelectTask = useCallback(
@@ -313,22 +315,30 @@ function useSheetActions(workspaceId: string | null, onOpenChange: (open: boolea
[loadTaskSessionsForTask, setActiveSession, setActiveTask, store, onOpenChange],
);
const [archivingTask, setArchivingTask] = useState<{ id: string; title: string } | null>(null);
const [isArchiving, setIsArchiving] = useState(false);
const handleArchiveTask = useCallback(
async (taskId: string) => {
// Capture active state before the async API call — the WS "task.updated"
// handler removes the archived task from kanbans before removeTaskFromBoard runs.
const { activeTaskId: wasActiveTaskId, activeSessionId: wasActiveSessionId } =
store.getState().tasks;
try {
await archiveTaskById(taskId);
await removeTaskFromBoard(taskId, { wasActiveTaskId, wasActiveSessionId });
} catch (error) {
console.error("Failed to archive task:", error);
}
(taskId: string) => {
const task = store.getState().kanban.tasks.find((t) => t.id === taskId);
setArchivingTask({ id: taskId, title: task?.title ?? "this task" });
},
[archiveTaskById, removeTaskFromBoard, store],
[store],
);
const handleArchiveConfirm = useCallback(async () => {
if (!archivingTask) return;
setIsArchiving(true);
try {
await archiveAndSwitch(archivingTask.id);
} catch (error) {
console.error("Failed to archive task:", error);
} finally {
setIsArchiving(false);
setArchivingTask(null);
}
}, [archivingTask, archiveAndSwitch]);
const handleDeleteTask = useCallback(
async (taskId: string) => {
setDeletingTaskId(taskId);
@@ -362,6 +372,10 @@ function useSheetActions(workspaceId: string | null, onOpenChange: (open: boolea
handleDeleteTask,
handleWorkspaceChange,
handleTaskCreated,
archivingTask,
setArchivingTask,
isArchiving,
handleArchiveConfirm,
};
}
@@ -388,6 +402,10 @@ export const SessionTaskSwitcherSheet = memo(function SessionTaskSwitcherSheet({
handleDeleteTask,
handleWorkspaceChange,
handleTaskCreated,
archivingTask,
setArchivingTask,
isArchiving,
handleArchiveConfirm,
} = useSheetActions(workspaceId, onOpenChange);
return (
@@ -448,6 +466,16 @@ export const SessionTaskSwitcherSheet = memo(function SessionTaskSwitcherSheet({
steps={dialogSteps}
onSuccess={handleTaskCreated}
/>
<ArchiveConfirmDialog
open={archivingTask !== null}
onOpenChange={(open) => {
if (!open) setArchivingTask(null);
}}
taskTitle={archivingTask?.title ?? ""}
isArchiving={isArchiving}
onConfirm={handleArchiveConfirm}
/>
</Sheet>
);
});
@@ -8,6 +8,7 @@ import type { GitStatusEntry } from "@/lib/state/slices/session-runtime/types";
import { TaskSwitcher } from "./task-switcher";
import type { TaskSwitcherItem } from "./task-switcher";
import { TaskRenameDialog } from "./task-rename-dialog";
import { ArchiveConfirmDialog } from "./archive-confirm-dialog";
import { Button } from "@kandev/ui/button";
import { PanelRoot, PanelBody } from "./panel-primitives";
import { IconPlus } from "@tabler/icons-react";
@@ -451,13 +452,41 @@ function useMoveToStep(store: StoreApi) {
);
}
function useArchiveActions(store: StoreApi) {
const archiveAndSwitch = useArchiveAndSwitchTask({ useLayoutSwitch: true });
const [archivingTask, setArchivingTask] = useState<{ id: string; title: string } | null>(null);
const [isArchiving, setIsArchiving] = useState(false);
const handleArchiveTask = useCallback(
(taskId: string) => {
const task = findTaskInSnapshots(store.getState().kanbanMulti.snapshots, taskId);
setArchivingTask({ id: taskId, title: task?.title ?? "this task" });
},
[store],
);
const handleArchiveConfirm = useCallback(async () => {
if (!archivingTask) return;
setIsArchiving(true);
try {
await archiveAndSwitch(archivingTask.id);
} catch (error) {
console.error("Failed to archive task:", error);
} finally {
setIsArchiving(false);
setArchivingTask(null);
}
}, [archivingTask, archiveAndSwitch]);
return { archivingTask, setArchivingTask, isArchiving, handleArchiveTask, handleArchiveConfirm };
}
function useSidebarActions(store: StoreApi) {
const setActiveTask = useAppStore((state) => state.setActiveTask);
const setActiveSession = useAppStore((state) => state.setActiveSession);
const [deletingTaskId, setDeletingTaskId] = useState<string | null>(null);
const [preparingTaskId, setPreparingTaskId] = useState<string | null>(null);
const { deleteTaskById, renameTaskById } = useTaskActions();
const archiveAndSwitch = useArchiveAndSwitchTask({ useLayoutSwitch: true });
const { removeTaskFromBoard, loadTaskSessionsForTask } = useTaskRemoval({
store,
useLayoutSwitch: true,
@@ -498,16 +527,7 @@ function useSidebarActions(store: StoreApi) {
[loadTaskSessionsForTask, switchToSession, setActiveTask, store],
);
const handleArchiveTask = useCallback(
async (taskId: string) => {
try {
await archiveAndSwitch(taskId);
} catch (error) {
console.error("Failed to archive task:", error);
}
},
[archiveAndSwitch],
);
const archiveActions = useArchiveActions(store);
const handleDeleteTask = useCallback(
async (taskId: string) => {
@@ -551,13 +571,13 @@ function useSidebarActions(store: StoreApi) {
deletingTaskId,
preparingTaskId,
handleSelectTask,
handleArchiveTask,
handleDeleteTask,
handleMoveToStep,
renamingTask,
setRenamingTask,
handleRenameTask,
handleRenameSubmit,
...archiveActions,
};
}
@@ -601,6 +621,10 @@ export const TaskSessionSidebar = memo(function TaskSessionSidebar({
setRenamingTask,
handleRenameTask,
handleRenameSubmit,
archivingTask,
setArchivingTask,
isArchiving,
handleArchiveConfirm,
} = useSidebarActions(store);
const displayTasks = useMemo(() => {
@@ -638,6 +662,15 @@ export const TaskSessionSidebar = memo(function TaskSessionSidebar({
currentTitle={renamingTask?.title ?? ""}
onSubmit={handleRenameSubmit}
/>
<ArchiveConfirmDialog
open={archivingTask !== null}
onOpenChange={(open) => {
if (!open) setArchivingTask(null);
}}
taskTitle={archivingTask?.title ?? ""}
isArchiving={isArchiving}
onConfirm={handleArchiveConfirm}
/>
</PanelRoot>
);
});