fix: preserve inherited workspace delete copy (#3864)
This commit is contained in:
@@ -9,12 +9,14 @@ import { Button } from "@kandev/ui/button";
|
|||||||
import { Badge } from "@kandev/ui/badge";
|
import { Badge } from "@kandev/ui/badge";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
|
||||||
import { formatTimeDistance, useDateLocale } from "@/lib/i18n/date-locale";
|
import { formatTimeDistance, useDateLocale } from "@/lib/i18n/date-locale";
|
||||||
|
import { cleanupSharesParentWorkspace } from "@/components/task/task-cleanup-summary";
|
||||||
import { TaskDeleteConfirmDialog } from "@/components/task/task-delete-confirm-dialog";
|
import { TaskDeleteConfirmDialog } from "@/components/task/task-delete-confirm-dialog";
|
||||||
import { TaskArchiveConfirmation } from "@/components/task/task-archive-confirmation";
|
import { TaskArchiveConfirmation } from "@/components/task/task-archive-confirmation";
|
||||||
import { linkToTask } from "@/lib/links";
|
import { linkToTask } from "@/lib/links";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { t } from "@/lib/i18n";
|
import { t } from "@/lib/i18n";
|
||||||
import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
|
import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
|
||||||
|
import { workspaceModeFromMetadata } from "@/lib/kanban/map-task";
|
||||||
|
|
||||||
type TaskWithResolution = Task & {
|
type TaskWithResolution = Task & {
|
||||||
workflowName?: string;
|
workflowName?: string;
|
||||||
@@ -132,6 +134,9 @@ function ActionsCell({ row, ctx }: { row: Row<TaskWithResolution>; ctx: ActionsC
|
|||||||
taskTitle={task.title}
|
taskTitle={task.title}
|
||||||
taskId={task.id}
|
taskId={task.id}
|
||||||
executorType={task.primary_executor_type}
|
executorType={task.primary_executor_type}
|
||||||
|
sharesParentWorkspace={cleanupSharesParentWorkspace(
|
||||||
|
workspaceModeFromMetadata(task.metadata),
|
||||||
|
)}
|
||||||
isDeleting={isDeleting}
|
isDeleting={isDeleting}
|
||||||
onConfirm={({ cascade, discardWorktreeChanges }) =>
|
onConfirm={({ cascade, discardWorktreeChanges }) =>
|
||||||
ctx.onDelete(task.id, { cascade, discardWorktreeChanges })
|
ctx.onDelete(task.id, { cascade, discardWorktreeChanges })
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Button } from "@kandev/ui/button";
|
|||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
|
||||||
import { IconArchive, IconArchiveOff, IconLoader, IconTrash } from "@tabler/icons-react";
|
import { IconArchive, IconArchiveOff, IconLoader, IconTrash } from "@tabler/icons-react";
|
||||||
import { TaskArchiveConfirmation } from "@/components/task/task-archive-confirmation";
|
import { TaskArchiveConfirmation } from "@/components/task/task-archive-confirmation";
|
||||||
|
import { cleanupSharesParentWorkspace } from "@/components/task/task-cleanup-summary";
|
||||||
import { TaskDeleteConfirmDialog } from "@/components/task/task-delete-confirm-dialog";
|
import { TaskDeleteConfirmDialog } from "@/components/task/task-delete-confirm-dialog";
|
||||||
import { primaryTaskRepository, type Repository, type Task, type Workflow } from "@/lib/types/http";
|
import { primaryTaskRepository, type Repository, type Task, type Workflow } from "@/lib/types/http";
|
||||||
import { formatTaskStateLabel } from "@/lib/ui/state-labels";
|
import { formatTaskStateLabel } from "@/lib/ui/state-labels";
|
||||||
@@ -20,6 +21,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { t } from "@/lib/i18n";
|
import { t } from "@/lib/i18n";
|
||||||
import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
|
import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
|
||||||
import type { TaskListFacetValue } from "@/lib/plugins/types";
|
import type { TaskListFacetValue } from "@/lib/plugins/types";
|
||||||
|
import { workspaceModeFromMetadata } from "@/lib/kanban/map-task";
|
||||||
|
|
||||||
export type TasksListViewProps = {
|
export type TasksListViewProps = {
|
||||||
total: number;
|
total: number;
|
||||||
@@ -548,6 +550,7 @@ function TaskRowActions({
|
|||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const workspaceMode = workspaceModeFromMetadata(task.metadata);
|
||||||
const archiveAnchorRef = useRef<HTMLButtonElement>(null);
|
const archiveAnchorRef = useRef<HTMLButtonElement>(null);
|
||||||
const { isFinePointer } = useResponsiveBreakpoint();
|
const { isFinePointer } = useResponsiveBreakpoint();
|
||||||
return (
|
return (
|
||||||
@@ -601,6 +604,7 @@ function TaskRowActions({
|
|||||||
taskId={task.id}
|
taskId={task.id}
|
||||||
isInFlight={isTaskInFlight(task.foreground_activity)}
|
isInFlight={isTaskInFlight(task.foreground_activity)}
|
||||||
executorType={task.primary_executor_type}
|
executorType={task.primary_executor_type}
|
||||||
|
sharesParentWorkspace={cleanupSharesParentWorkspace(workspaceMode)}
|
||||||
isDeleting={isDeleting}
|
isDeleting={isDeleting}
|
||||||
onConfirm={({ cascade, discardWorktreeChanges }) =>
|
onConfirm={({ cascade, discardWorktreeChanges }) =>
|
||||||
onDelete(task.id, { cascade, discardWorktreeChanges })
|
onDelete(task.id, { cascade, discardWorktreeChanges })
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
useKanbanCardMoveTargets,
|
useKanbanCardMoveTargets,
|
||||||
} from "@/components/kanban-card-menu-items";
|
} from "@/components/kanban-card-menu-items";
|
||||||
import { useTaskPluginLinkActions } from "@/components/task/task-session-sidebar-link-actions";
|
import { useTaskPluginLinkActions } from "@/components/task/task-session-sidebar-link-actions";
|
||||||
|
import { cleanupSharesParentWorkspace } from "@/components/task/task-cleanup-summary";
|
||||||
import { TaskDeleteConfirmDialog } from "@/components/task/task-delete-confirm-dialog";
|
import { TaskDeleteConfirmDialog } from "@/components/task/task-delete-confirm-dialog";
|
||||||
import {
|
import {
|
||||||
TaskExternalLinkDialog,
|
TaskExternalLinkDialog,
|
||||||
@@ -269,6 +270,7 @@ export function KanbanCardDialogs({
|
|||||||
taskTitle={task.title}
|
taskTitle={task.title}
|
||||||
taskId={task.id}
|
taskId={task.id}
|
||||||
executorType={task.primaryExecutorType}
|
executorType={task.primaryExecutorType}
|
||||||
|
sharesParentWorkspace={cleanupSharesParentWorkspace(task.workspaceMode)}
|
||||||
isDeleting={isDeleting}
|
isDeleting={isDeleting}
|
||||||
onConfirm={(opts) => onDelete?.(task, opts)}
|
onConfirm={(opts) => onDelete?.(task, opts)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useRef, type RefObject } from "react";
|
|||||||
import { useActiveWorkspaceRepositories } from "@/components/kanban-card-repositories";
|
import { useActiveWorkspaceRepositories } from "@/components/kanban-card-repositories";
|
||||||
import { TaskArchiveConfirmation } from "@/components/task/task-archive-confirmation";
|
import { TaskArchiveConfirmation } from "@/components/task/task-archive-confirmation";
|
||||||
import { TaskCreateDialog } from "@/components/task-create-dialog";
|
import { TaskCreateDialog } from "@/components/task-create-dialog";
|
||||||
|
import { cleanupSharesParentWorkspace } from "@/components/task/task-cleanup-summary";
|
||||||
import { TaskDeleteConfirmDialog } from "@/components/task/task-delete-confirm-dialog";
|
import { TaskDeleteConfirmDialog } from "@/components/task/task-delete-confirm-dialog";
|
||||||
import { TaskDetachConfirmationSurface } from "@/components/task/task-detach-confirm-dialog";
|
import { TaskDetachConfirmationSurface } from "@/components/task/task-detach-confirm-dialog";
|
||||||
import { TaskExternalLinkDialog } from "@/components/task/task-external-link-dialog";
|
import { TaskExternalLinkDialog } from "@/components/task/task-external-link-dialog";
|
||||||
@@ -122,6 +123,20 @@ function useLastResolvedBoardRow(
|
|||||||
return ref.current.boardRow;
|
return ref.current.boardRow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildConfirmationTarget(
|
||||||
|
taskId: string,
|
||||||
|
taskTitle: string,
|
||||||
|
triggerRef: RefObject<HTMLElement | null>,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
taskId,
|
||||||
|
taskTitle,
|
||||||
|
anchorRef: triggerRef,
|
||||||
|
focusReturnRef: triggerRef,
|
||||||
|
restoreFocusOnConfirm: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mounts every dialog a task actions menu entry can open. Hosted per surface
|
* Mounts every dialog a task actions menu entry can open. Hosted per surface
|
||||||
* so a dialog outlives the menu that opened it.
|
* so a dialog outlives the menu that opened it.
|
||||||
@@ -140,13 +155,7 @@ export function TaskActionsMenuDialogs({
|
|||||||
const editBoardRow = useLastResolvedBoardRow(taskId, boardRow);
|
const editBoardRow = useLastResolvedBoardRow(taskId, boardRow);
|
||||||
if (!taskId) return null;
|
if (!taskId) return null;
|
||||||
const linkDialogTask = buildLinkDialogTask(taskId, taskTitle, boardRow);
|
const linkDialogTask = buildLinkDialogTask(taskId, taskTitle, boardRow);
|
||||||
const confirmationTarget = {
|
const confirmationTarget = buildConfirmationTarget(taskId, taskTitle, menu.triggerRef);
|
||||||
taskId,
|
|
||||||
taskTitle,
|
|
||||||
anchorRef: menu.triggerRef,
|
|
||||||
focusReturnRef: menu.triggerRef,
|
|
||||||
restoreFocusOnConfirm: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -175,6 +184,7 @@ export function TaskActionsMenuDialogs({
|
|||||||
taskTitle={taskTitle}
|
taskTitle={taskTitle}
|
||||||
taskId={taskId}
|
taskId={taskId}
|
||||||
executorType={boardRow?.primaryExecutorType ?? subjectExecutorType}
|
executorType={boardRow?.primaryExecutorType ?? subjectExecutorType}
|
||||||
|
sharesParentWorkspace={cleanupSharesParentWorkspace(boardRow?.workspaceMode)}
|
||||||
isDeleting={isDeleting}
|
isDeleting={isDeleting}
|
||||||
onConfirm={(opts) => menu.onConfirmDelete(opts)}
|
onConfirm={(opts) => menu.onConfirmDelete(opts)}
|
||||||
focusReturnRef={menu.triggerRef}
|
focusReturnRef={menu.triggerRef}
|
||||||
|
|||||||
@@ -25,6 +25,15 @@ describe("getCleanupSummary (single task)", () => {
|
|||||||
expect(notes.join(" ")).toMatch(/not affected/i);
|
expect(notes.join(" ")).toMatch(/not affected/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preserves the parent workspace for inherited-parent tasks", () => {
|
||||||
|
expect(getCleanupSummary("worktree", { sharesParentWorkspace: true })).toEqual({
|
||||||
|
effects: [AGENT_STOP_EFFECT],
|
||||||
|
notes: [
|
||||||
|
"This task shares its parent's workspace. The parent task's worktree, branch, and files are not touched.",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("describes local_docker container removal", () => {
|
it("describes local_docker container removal", () => {
|
||||||
const { effects, notes } = getCleanupSummary("local_docker");
|
const { effects, notes } = getCleanupSummary("local_docker");
|
||||||
expect(effects[0]).toMatch(/Docker container/i);
|
expect(effects[0]).toMatch(/Docker container/i);
|
||||||
|
|||||||
@@ -1,11 +1,22 @@
|
|||||||
// Executor types match models.ExecutorType in apps/backend/internal/task/models/models.go.
|
// Executor types match models.ExecutorType in apps/backend/internal/task/models/models.go.
|
||||||
import { t } from "@/lib/i18n";
|
import { t } from "@/lib/i18n";
|
||||||
|
import type { WorkspaceMode } from "@/lib/kanban/map-task";
|
||||||
|
|
||||||
export type CleanupSummary = {
|
export type CleanupSummary = {
|
||||||
effects: string[];
|
effects: string[];
|
||||||
notes: string[];
|
notes: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CleanupSummaryOptions = {
|
||||||
|
sharesParentWorkspace?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function cleanupSharesParentWorkspace(
|
||||||
|
workspaceMode: WorkspaceMode | null | undefined,
|
||||||
|
): boolean | undefined {
|
||||||
|
return workspaceMode == null ? undefined : workspaceMode === "inherit_parent";
|
||||||
|
}
|
||||||
|
|
||||||
// mock_remote is test-only and intentionally falls through to the generic effect.
|
// mock_remote is test-only and intentionally falls through to the generic effect.
|
||||||
type KnownExecutor =
|
type KnownExecutor =
|
||||||
| "local"
|
| "local"
|
||||||
@@ -45,6 +56,7 @@ const SINGLE_COPIES: Record<KnownExecutor, CleanupCopy> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const GENERIC_EFFECT_KEY = "task:cleanupAgentSessionsStopped";
|
const GENERIC_EFFECT_KEY = "task:cleanupAgentSessionsStopped";
|
||||||
|
const INHERITED_PARENT_WORKSPACE_NOTE_KEY = "task:cleanupInheritedParentWorkspaceNote";
|
||||||
|
|
||||||
function normalize(executorType: string | null | undefined): KnownExecutor | null {
|
function normalize(executorType: string | null | undefined): KnownExecutor | null {
|
||||||
if (!executorType) return null;
|
if (!executorType) return null;
|
||||||
@@ -65,7 +77,16 @@ function resolveCopy(copy: CleanupCopy, options?: { count: number }): CleanupSum
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Single-task variant. */
|
/** Single-task variant. */
|
||||||
export function getCleanupSummary(executorType: string | null | undefined): CleanupSummary {
|
export function getCleanupSummary(
|
||||||
|
executorType: string | null | undefined,
|
||||||
|
options?: CleanupSummaryOptions,
|
||||||
|
): CleanupSummary {
|
||||||
|
if (options?.sharesParentWorkspace) {
|
||||||
|
return {
|
||||||
|
effects: [t(GENERIC_EFFECT_KEY)],
|
||||||
|
notes: [t(INHERITED_PARENT_WORKSPACE_NOTE_KEY)],
|
||||||
|
};
|
||||||
|
}
|
||||||
const known = normalize(executorType);
|
const known = normalize(executorType);
|
||||||
return known
|
return known
|
||||||
? resolveCopy(SINGLE_COPIES[known])
|
? resolveCopy(SINGLE_COPIES[known])
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ vi.mock("@/lib/api", () => ({
|
|||||||
import { TaskDeleteConfirmDialog } from "./task-delete-confirm-dialog";
|
import { TaskDeleteConfirmDialog } from "./task-delete-confirm-dialog";
|
||||||
import { expectCompactWarning } from "./task-confirm-dialog.test-helpers";
|
import { expectCompactWarning } from "./task-confirm-dialog.test-helpers";
|
||||||
|
|
||||||
type SeedTask = { id: string; foregroundActivity?: "generating" | "background" | null };
|
type SeedTask = {
|
||||||
|
id: string;
|
||||||
|
foregroundActivity?: "generating" | "background" | null;
|
||||||
|
workspaceMode?: "inherit_parent" | "new_workspace" | "shared_group";
|
||||||
|
};
|
||||||
|
|
||||||
// The dialog reads live foreground_activity from the store via useTaskInFlight,
|
// The dialog reads live foreground_activity from the store via useTaskInFlight,
|
||||||
// so every render needs a StateProvider. `tasks` seeds the active kanban tasks
|
// so every render needs a StateProvider. `tasks` seeds the active kanban tasks
|
||||||
@@ -33,6 +37,7 @@ function renderDialog(ui: ReactNode, tasks: SeedTask[] = []) {
|
|||||||
title: t.id,
|
title: t.id,
|
||||||
position: 0,
|
position: 0,
|
||||||
foregroundActivity: t.foregroundActivity ?? undefined,
|
foregroundActivity: t.foregroundActivity ?? undefined,
|
||||||
|
workspaceMode: t.workspaceMode,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -444,6 +449,72 @@ describe("TaskDeleteConfirmDialog executor cleanup copy", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("TaskDeleteConfirmDialog inherited-parent workspace copy", () => {
|
||||||
|
it("describes inherited-parent workspace preservation from explicit context", () => {
|
||||||
|
mockGetSubtaskCount.mockResolvedValue({ count: 0 });
|
||||||
|
renderDialog(
|
||||||
|
<TaskDeleteConfirmDialog
|
||||||
|
open
|
||||||
|
onOpenChange={() => {}}
|
||||||
|
taskTitle="Child task"
|
||||||
|
taskId={TASK_ID}
|
||||||
|
executorType="worktree"
|
||||||
|
sharesParentWorkspace
|
||||||
|
onConfirm={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByTestId("task-cleanup-effects").querySelectorAll("li")).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
/This task shares its parent's workspace\. The parent task's worktree, branch, and files are not touched\./i,
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(screen.queryByText(/worktree and its branch will be deleted/i)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves inherited-parent workspace preservation from the task store", () => {
|
||||||
|
mockGetSubtaskCount.mockResolvedValue({ count: 0 });
|
||||||
|
renderDialog(
|
||||||
|
<TaskDeleteConfirmDialog
|
||||||
|
open
|
||||||
|
onOpenChange={() => {}}
|
||||||
|
taskTitle="Child task"
|
||||||
|
taskId={TASK_ID}
|
||||||
|
executorType="worktree"
|
||||||
|
onConfirm={() => {}}
|
||||||
|
/>,
|
||||||
|
[{ id: TASK_ID, workspaceMode: "inherit_parent" }],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
/This task shares its parent's workspace\. The parent task's worktree, branch, and files are not touched\./i,
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(screen.queryByText(/worktree and its branch will be deleted/i)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers the live task-store mode over stale caller context", () => {
|
||||||
|
mockGetSubtaskCount.mockResolvedValue({ count: 0 });
|
||||||
|
renderDialog(
|
||||||
|
<TaskDeleteConfirmDialog
|
||||||
|
open
|
||||||
|
onOpenChange={() => {}}
|
||||||
|
taskTitle="Child task"
|
||||||
|
taskId={TASK_ID}
|
||||||
|
executorType="worktree"
|
||||||
|
sharesParentWorkspace
|
||||||
|
onConfirm={() => {}}
|
||||||
|
/>,
|
||||||
|
[{ id: TASK_ID, workspaceMode: "new_workspace" }],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText(/worktree and its branch will be deleted/i)).toBeTruthy();
|
||||||
|
expect(screen.queryByText(/shares its parent's workspace/i)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("TaskDeleteConfirmDialog still-working guard", () => {
|
describe("TaskDeleteConfirmDialog still-working guard", () => {
|
||||||
it("keeps the shared in-flight warning visually subordinate", () => {
|
it("keeps the shared in-flight warning visually subordinate", () => {
|
||||||
mockGetSubtaskCount.mockResolvedValue({ count: 0 });
|
mockGetSubtaskCount.mockResolvedValue({ count: 0 });
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import { useTaskInFlight } from "@/hooks/use-task-in-flight";
|
|||||||
import { getCleanupSummary, getBulkCleanupSummary } from "./task-cleanup-summary";
|
import { getCleanupSummary, getBulkCleanupSummary } from "./task-cleanup-summary";
|
||||||
import { TaskCleanupConsequences } from "./task-cleanup-consequences";
|
import { TaskCleanupConsequences } from "./task-cleanup-consequences";
|
||||||
import { StillWorkingWarning } from "./task-still-working-warning";
|
import { StillWorkingWarning } from "./task-still-working-warning";
|
||||||
|
import { useAppStore } from "@/components/state-provider";
|
||||||
|
import { findTaskInSnapshots } from "@/lib/kanban/find-task";
|
||||||
import {
|
import {
|
||||||
TASK_CONFIRM_ACTION_CLASS,
|
TASK_CONFIRM_ACTION_CLASS,
|
||||||
TASK_CONFIRM_BODY_CLASS,
|
TASK_CONFIRM_BODY_CLASS,
|
||||||
@@ -34,6 +36,14 @@ import {
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { createFocusReturnHandler } from "@/lib/dialog-focus-return";
|
import { createFocusReturnHandler } from "@/lib/dialog-focus-return";
|
||||||
|
|
||||||
|
function useStoreSharesParentWorkspace(taskId: string | undefined, isBulkOperation?: boolean) {
|
||||||
|
return useAppStore((state) => {
|
||||||
|
if (isBulkOperation || !taskId) return undefined;
|
||||||
|
const task = findTaskInSnapshots(taskId, state.kanbanMulti.snapshots, state.kanban.tasks);
|
||||||
|
return task ? task.workspaceMode === "inherit_parent" : undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
type TaskDeleteConfirmDialogProps = {
|
type TaskDeleteConfirmDialogProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
@@ -48,6 +58,8 @@ type TaskDeleteConfirmDialogProps = {
|
|||||||
executorType?: string | null;
|
executorType?: string | null;
|
||||||
/** Executor types of the tasks being deleted (bulk). */
|
/** Executor types of the tasks being deleted (bulk). */
|
||||||
executorTypes?: Array<string | null | undefined>;
|
executorTypes?: Array<string | null | undefined>;
|
||||||
|
/** Whether the single task borrows its workspace from its parent. */
|
||||||
|
sharesParentWorkspace?: boolean;
|
||||||
onConfirm: (opts: { cascade: boolean; discardWorktreeChanges: boolean }) => void;
|
onConfirm: (opts: { cascade: boolean; discardWorktreeChanges: boolean }) => void;
|
||||||
confirmTestId?: string;
|
confirmTestId?: string;
|
||||||
/** Overrides default focus restoration when the original trigger may disappear. */
|
/** Overrides default focus restoration when the original trigger may disappear. */
|
||||||
@@ -284,6 +296,7 @@ export function TaskDeleteConfirmDialog({
|
|||||||
isInFlight,
|
isInFlight,
|
||||||
executorType,
|
executorType,
|
||||||
executorTypes,
|
executorTypes,
|
||||||
|
sharesParentWorkspace,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
confirmTestId,
|
confirmTestId,
|
||||||
onCloseAutoFocus,
|
onCloseAutoFocus,
|
||||||
@@ -297,9 +310,12 @@ export function TaskDeleteConfirmDialog({
|
|||||||
const description = isBulkOperation
|
const description = isBulkOperation
|
||||||
? t("task:deleteTasksConfirm", { count: safeCount })
|
? t("task:deleteTasksConfirm", { count: safeCount })
|
||||||
: t("task:deleteTaskConfirm", { taskTitle });
|
: t("task:deleteTaskConfirm", { taskTitle });
|
||||||
|
const storeSharesParentWorkspace = useStoreSharesParentWorkspace(taskId, isBulkOperation);
|
||||||
const cleanup = isBulkOperation
|
const cleanup = isBulkOperation
|
||||||
? getBulkCleanupSummary(executorTypes ?? [])
|
? getBulkCleanupSummary(executorTypes ?? [])
|
||||||
: getCleanupSummary(executorType);
|
: getCleanupSummary(executorType, {
|
||||||
|
sharesParentWorkspace: storeSharesParentWorkspace ?? sharesParentWorkspace,
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
cascade,
|
cascade,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import type { TaskPriority } from "@/lib/types/http";
|
|||||||
import { TaskManagementMenu, type TaskManagementMenuProps } from "./task-management-menu";
|
import { TaskManagementMenu, type TaskManagementMenuProps } from "./task-management-menu";
|
||||||
import { TaskManagementDrawer, TaskManagementSheet } from "./task-management-drawer";
|
import { TaskManagementDrawer, TaskManagementSheet } from "./task-management-drawer";
|
||||||
import { TaskArchiveConfirmation } from "./task-archive-confirmation";
|
import { TaskArchiveConfirmation } from "./task-archive-confirmation";
|
||||||
|
import { cleanupSharesParentWorkspace } from "./task-cleanup-summary";
|
||||||
import { TaskDeleteConfirmDialog } from "./task-delete-confirm-dialog";
|
import { TaskDeleteConfirmDialog } from "./task-delete-confirm-dialog";
|
||||||
import { useSidebarLinkActions } from "./task-session-sidebar-link-actions";
|
import { useSidebarLinkActions } from "./task-session-sidebar-link-actions";
|
||||||
import { useSidebarTaskLinking } from "./task-session-sidebar-task-linking";
|
import { useSidebarTaskLinking } from "./task-session-sidebar-task-linking";
|
||||||
@@ -215,6 +216,7 @@ function ManagementConfirmations({
|
|||||||
taskId={task.id}
|
taskId={task.id}
|
||||||
taskTitle={task.title}
|
taskTitle={task.title}
|
||||||
executorType={task.remoteExecutorType}
|
executorType={task.remoteExecutorType}
|
||||||
|
sharesParentWorkspace={cleanupSharesParentWorkspace(task.workspaceMode)}
|
||||||
isDeleting={mutations.disabled}
|
isDeleting={mutations.disabled}
|
||||||
onCloseAutoFocus={onCloseAutoFocus}
|
onCloseAutoFocus={onCloseAutoFocus}
|
||||||
confirmTestId="thread-delete-confirm"
|
confirmTestId="thread-delete-confirm"
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import { createAppStore } from "@/lib/state/store";
|
|||||||
import { resolveTaskMenuTarget } from "./task-menu-target";
|
import { resolveTaskMenuTarget } from "./task-menu-target";
|
||||||
|
|
||||||
const identity = { taskId: "A", workspaceId: "workspace" };
|
const identity = { taskId: "A", workspaceId: "workspace" };
|
||||||
function fixture(loss?: string) {
|
function fixture(
|
||||||
|
loss?: string,
|
||||||
|
workspaceMode?: "inherit_parent" | "new_workspace" | "shared_group",
|
||||||
|
) {
|
||||||
const store = createAppStore();
|
const store = createAppStore();
|
||||||
store.setState((state) => ({
|
store.setState((state) => ({
|
||||||
workspaces: { ...state.workspaces, activeId: loss === "workspace" ? "elsewhere" : "workspace" },
|
workspaces: { ...state.workspaces, activeId: loss === "workspace" ? "elsewhere" : "workspace" },
|
||||||
@@ -32,6 +35,7 @@ function fixture(loss?: string) {
|
|||||||
priority: "high",
|
priority: "high",
|
||||||
isArchived: loss === "archived",
|
isArchived: loss === "archived",
|
||||||
primaryExecutorType: "worktree",
|
primaryExecutorType: "worktree",
|
||||||
|
workspaceMode,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -61,4 +65,9 @@ describe("task menu eligibility", () => {
|
|||||||
expect(resolveTaskMenuTarget(state, identity)).toBeNull();
|
expect(resolveTaskMenuTarget(state, identity)).toBeNull();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it("carries the task workspace mode into the management target", () => {
|
||||||
|
const state = fixture(undefined, "inherit_parent").getState();
|
||||||
|
expect(resolveTaskMenuTarget(state, identity)?.workspaceMode).toBe("inherit_parent");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,5 +32,6 @@ export function resolveTaskMenuTarget(
|
|||||||
remoteExecutorType: task.primaryExecutorType ?? undefined,
|
remoteExecutorType: task.primaryExecutorType ?? undefined,
|
||||||
primarySessionId: task.primarySessionId,
|
primarySessionId: task.primarySessionId,
|
||||||
parentTaskId: task.parentTaskId ?? undefined,
|
parentTaskId: task.parentTaskId ?? undefined,
|
||||||
|
workspaceMode: task.workspaceMode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2237,6 +2237,7 @@
|
|||||||
"cleanupSingleSprites": "The Sprites cloud sandbox for this task will be destroyed. Any uncommitted work inside it will be lost.",
|
"cleanupSingleSprites": "The Sprites cloud sandbox for this task will be destroyed. Any uncommitted work inside it will be lost.",
|
||||||
"cleanupSingleSsh": "The task directory on the remote host will be removed.",
|
"cleanupSingleSsh": "The task directory on the remote host will be removed.",
|
||||||
"cleanupSingleSshNote": "Best-effort removal may not complete. Your local repo is not touched.",
|
"cleanupSingleSshNote": "Best-effort removal may not complete. Your local repo is not touched.",
|
||||||
|
"cleanupInheritedParentWorkspaceNote": "This task shares its parent's workspace. The parent task's worktree, branch, and files are not touched.",
|
||||||
"cleanupSingleWorktree": "The task's git worktree and its branch will be deleted.",
|
"cleanupSingleWorktree": "The task's git worktree and its branch will be deleted.",
|
||||||
"cleanupSingleWorktreeNote": "Your main repo and other branches are not affected.",
|
"cleanupSingleWorktreeNote": "Your main repo and other branches are not affected.",
|
||||||
"enhancedPromptCopiedToClipboard": "Enhanced prompt copied to clipboard.",
|
"enhancedPromptCopiedToClipboard": "Enhanced prompt copied to clipboard.",
|
||||||
|
|||||||
@@ -2237,6 +2237,7 @@
|
|||||||
"cleanupSingleSprites": "このタスクの Sprites クラウドサンドボックスは破棄されます。中の未コミット作業は失われます。",
|
"cleanupSingleSprites": "このタスクの Sprites クラウドサンドボックスは破棄されます。中の未コミット作業は失われます。",
|
||||||
"cleanupSingleSsh": "リモートホスト上のタスクディレクトリが削除されます。",
|
"cleanupSingleSsh": "リモートホスト上のタスクディレクトリが削除されます。",
|
||||||
"cleanupSingleSshNote": "ベストエフォート削除なので、完了しない場合があります。ローカルリポジトリには触れません。",
|
"cleanupSingleSshNote": "ベストエフォート削除なので、完了しない場合があります。ローカルリポジトリには触れません。",
|
||||||
|
"cleanupInheritedParentWorkspaceNote": "このタスクは親タスクのワークスペースを共有します。親タスクの worktree、ブランチ、ファイルには触れません。",
|
||||||
"cleanupSingleWorktree": "タスクの git worktree とそのブランチが削除されます。",
|
"cleanupSingleWorktree": "タスクの git worktree とそのブランチが削除されます。",
|
||||||
"cleanupSingleWorktreeNote": "メインリポジトリと他のブランチには影響しません。",
|
"cleanupSingleWorktreeNote": "メインリポジトリと他のブランチには影響しません。",
|
||||||
"enhancedPromptCopiedToClipboard": "拡張プロンプトをクリップボードにコピーしました。",
|
"enhancedPromptCopiedToClipboard": "拡張プロンプトをクリップボードにコピーしました。",
|
||||||
|
|||||||
@@ -2237,6 +2237,7 @@
|
|||||||
"cleanupSingleSprites": "Ţĥē Śƥŕĩţēś ćĺōũď śàńďƀōx ƒōŕ ţĥĩś ţàśķ ŵĩĺĺ ƀē ďēśţŕōŷēď. Àńŷ ũńćōḿḿĩţţēď ŵōŕķ ĩńśĩďē ĩţ ŵĩĺĺ ƀē ĺōśţ.",
|
"cleanupSingleSprites": "Ţĥē Śƥŕĩţēś ćĺōũď śàńďƀōx ƒōŕ ţĥĩś ţàśķ ŵĩĺĺ ƀē ďēśţŕōŷēď. Àńŷ ũńćōḿḿĩţţēď ŵōŕķ ĩńśĩďē ĩţ ŵĩĺĺ ƀē ĺōśţ.",
|
||||||
"cleanupSingleSsh": "Ţĥē ţàśķ ďĩŕēćţōŕŷ ōń ţĥē ŕēḿōţē ĥōśţ ŵĩĺĺ ƀē ŕēḿōvēď.",
|
"cleanupSingleSsh": "Ţĥē ţàśķ ďĩŕēćţōŕŷ ōń ţĥē ŕēḿōţē ĥōśţ ŵĩĺĺ ƀē ŕēḿōvēď.",
|
||||||
"cleanupSingleSshNote": "Ɓēśţ-ēƒƒōŕţ ŕēḿōvàĺ ḿàŷ ńōţ ćōḿƥĺēţē. Ŷōũŕ ĺōćàĺ ŕēƥō ĩś ńōţ ţōũćĥēď.",
|
"cleanupSingleSshNote": "Ɓēśţ-ēƒƒōŕţ ŕēḿōvàĺ ḿàŷ ńōţ ćōḿƥĺēţē. Ŷōũŕ ĺōćàĺ ŕēƥō ĩś ńōţ ţōũćĥēď.",
|
||||||
|
"cleanupInheritedParentWorkspaceNote": "Ţĥĩś ţàśķ śĥàŕēś ĩţś ƥàŕēńţ'ś ŵōŕķśƥàćē. Ţĥē ƥàŕēńţ ţàśķ'ś ŵōŕķţŕēē, ƀŕàńćĥ, àńď ƒĩĺēś àŕē ńōţ ţōũćĥēď.",
|
||||||
"cleanupSingleWorktree": "Ţĥē ţàśķ'ś ĝĩţ ŵōŕķţŕēē àńď ĩţś ƀŕàńćĥ ŵĩĺĺ ƀē ďēĺēţēď.",
|
"cleanupSingleWorktree": "Ţĥē ţàśķ'ś ĝĩţ ŵōŕķţŕēē àńď ĩţś ƀŕàńćĥ ŵĩĺĺ ƀē ďēĺēţēď.",
|
||||||
"cleanupSingleWorktreeNote": "Ŷōũŕ ḿàĩń ŕēƥō àńď ōţĥēŕ ƀŕàńćĥēś àŕē ńōţ àƒƒēćţēď.",
|
"cleanupSingleWorktreeNote": "Ŷōũŕ ḿàĩń ŕēƥō àńď ōţĥēŕ ƀŕàńćĥēś àŕē ńōţ àƒƒēćţēď.",
|
||||||
"enhancedPromptCopiedToClipboard": "Ēńĥàńćēď ƥŕōḿƥţ ćōƥĩēď ţō ćĺĩƥƀōàŕď.",
|
"enhancedPromptCopiedToClipboard": "Ēńĥàńćēď ƥŕōḿƥţ ćōƥĩēď ţō ćĺĩƥƀōàŕď.",
|
||||||
|
|||||||
@@ -2101,6 +2101,7 @@
|
|||||||
"cleanupSingleSprites": "A sandbox na cloud Sprites desta tarefa será destruída. Todo o trabalho não commitado lá dentro será perdido.",
|
"cleanupSingleSprites": "A sandbox na cloud Sprites desta tarefa será destruída. Todo o trabalho não commitado lá dentro será perdido.",
|
||||||
"cleanupSingleSsh": "O diretório da tarefa no anfitrião remoto será removido.",
|
"cleanupSingleSsh": "O diretório da tarefa no anfitrião remoto será removido.",
|
||||||
"cleanupSingleSshNote": "A remoção é uma tentativa e pode não ser concluída. O seu repositório local não é tocado.",
|
"cleanupSingleSshNote": "A remoção é uma tentativa e pode não ser concluída. O seu repositório local não é tocado.",
|
||||||
|
"cleanupInheritedParentWorkspaceNote": "Esta tarefa partilha o espaço de trabalho da tarefa principal. O worktree, o ramo e os ficheiros da tarefa principal não serão tocados.",
|
||||||
"cleanupSingleWorktree": "O worktree git da tarefa e o seu ramo serão eliminados.",
|
"cleanupSingleWorktree": "O worktree git da tarefa e o seu ramo serão eliminados.",
|
||||||
"cleanupSingleWorktreeNote": "O seu repositório principal e os outros ramos não são afetados.",
|
"cleanupSingleWorktreeNote": "O seu repositório principal e os outros ramos não são afetados.",
|
||||||
"composerPlaceholderBusy": "Colocar instruções na fila para o agente...",
|
"composerPlaceholderBusy": "Colocar instruções na fila para o agente...",
|
||||||
|
|||||||
@@ -2101,6 +2101,7 @@
|
|||||||
"cleanupSingleSprites": "该任务的 Sprites 云沙箱将被销毁。其中所有未提交的工作都会丢失。",
|
"cleanupSingleSprites": "该任务的 Sprites 云沙箱将被销毁。其中所有未提交的工作都会丢失。",
|
||||||
"cleanupSingleSsh": "远程主机上的任务目录将被移除。",
|
"cleanupSingleSsh": "远程主机上的任务目录将被移除。",
|
||||||
"cleanupSingleSshNote": "尽力移除,可能无法完成。你的本地仓库不会被改动。",
|
"cleanupSingleSshNote": "尽力移除,可能无法完成。你的本地仓库不会被改动。",
|
||||||
|
"cleanupInheritedParentWorkspaceNote": "此任务与父任务共享工作区。父任务的 worktree、分支和文件不会受到影响。",
|
||||||
"cleanupSingleWorktree": "该任务的 git worktree 及其分支将被删除。",
|
"cleanupSingleWorktree": "该任务的 git worktree 及其分支将被删除。",
|
||||||
"cleanupSingleWorktreeNote": "你的主仓库和其他分支不受影响。",
|
"cleanupSingleWorktreeNote": "你的主仓库和其他分支不受影响。",
|
||||||
"composerPlaceholderBusy": "为智能体排队指令…",
|
"composerPlaceholderBusy": "为智能体排队指令…",
|
||||||
|
|||||||
@@ -2101,6 +2101,7 @@
|
|||||||
"cleanupSingleSprites": "該任務的 Sprites 雲沙箱將被銷燬。其中所有未提交的工作都會丟失。",
|
"cleanupSingleSprites": "該任務的 Sprites 雲沙箱將被銷燬。其中所有未提交的工作都會丟失。",
|
||||||
"cleanupSingleSsh": "遠端主機上的任務目錄將被移除。",
|
"cleanupSingleSsh": "遠端主機上的任務目錄將被移除。",
|
||||||
"cleanupSingleSshNote": "盡力移除,可能無法完成。你的本地儲存庫不會被改動。",
|
"cleanupSingleSshNote": "盡力移除,可能無法完成。你的本地儲存庫不會被改動。",
|
||||||
|
"cleanupInheritedParentWorkspaceNote": "此任務與父任務共享工作區。父任務的 worktree、分支和檔案不會受到影響。",
|
||||||
"cleanupSingleWorktree": "該任務的 git worktree 及其分支將被刪除。",
|
"cleanupSingleWorktree": "該任務的 git worktree 及其分支將被刪除。",
|
||||||
"cleanupSingleWorktreeNote": "你的主儲存庫和其他分支不受影響。",
|
"cleanupSingleWorktreeNote": "你的主儲存庫和其他分支不受影響。",
|
||||||
"composerPlaceholderBusy": "為代理程式排隊指令…",
|
"composerPlaceholderBusy": "為代理程式排隊指令…",
|
||||||
|
|||||||
@@ -2101,6 +2101,7 @@
|
|||||||
"cleanupSingleSprites": "該任務的 Sprites 雲沙箱將被銷燬。其中所有未提交的工作都會丟失。",
|
"cleanupSingleSprites": "該任務的 Sprites 雲沙箱將被銷燬。其中所有未提交的工作都會丟失。",
|
||||||
"cleanupSingleSsh": "遠端主機上的任務目錄將被移除。",
|
"cleanupSingleSsh": "遠端主機上的任務目錄將被移除。",
|
||||||
"cleanupSingleSshNote": "盡力移除,可能無法完成。你的本地儲存庫不會被改動。",
|
"cleanupSingleSshNote": "盡力移除,可能無法完成。你的本地儲存庫不會被改動。",
|
||||||
|
"cleanupInheritedParentWorkspaceNote": "此任務與父任務共享工作區。父任務的 worktree、分支和檔案不會受到影響。",
|
||||||
"cleanupSingleWorktree": "該任務的 git worktree 及其分支將被刪除。",
|
"cleanupSingleWorktree": "該任務的 git worktree 及其分支將被刪除。",
|
||||||
"cleanupSingleWorktreeNote": "你的主儲存庫和其他分支不受影響。",
|
"cleanupSingleWorktreeNote": "你的主儲存庫和其他分支不受影響。",
|
||||||
"composerPlaceholderBusy": "為代理程式排隊指令…",
|
"composerPlaceholderBusy": "為代理程式排隊指令…",
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
---
|
||||||
|
created: 2026-09-22
|
||||||
|
status: complete
|
||||||
|
requirements:
|
||||||
|
- REQ-UI-TASK-CLEANUP-CONFIRMATION-001
|
||||||
|
- REQ-TASKS-RUNTIME-CLEANUP-001
|
||||||
|
system_design:
|
||||||
|
- ../../specs/ui/system-design/confirmation-warning-hierarchy.md
|
||||||
|
- ../../specs/tasks/system-design/runtime-cleanup.md
|
||||||
|
legacy_specs: []
|
||||||
|
---
|
||||||
|
|
||||||
|
# Implementation Plan: Preserve inherited parent workspace copy
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Correct single-task delete confirmation copy when a subtask uses
|
||||||
|
`workspaceMode: "inherit_parent"`. The existing cleanup model keys only on the
|
||||||
|
executor and therefore tells users that the child's worktree and branch will be
|
||||||
|
deleted, even though task cleanup preserves the borrowed parent environment.
|
||||||
|
The implementation updates the shared summary model, the delete dialog's
|
||||||
|
explicit and task-store-derived context, all specified task-aware callers, and
|
||||||
|
the supported locale catalogs.
|
||||||
|
|
||||||
|
The work is one sequential vertical slice. The existing backend cleanup
|
||||||
|
contract remains unchanged; this package makes the confirmation reflect it.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### In scope
|
||||||
|
|
||||||
|
- Add the inherited-parent cleanup summary branch and localized note.
|
||||||
|
- Resolve inherited-parent state explicitly at task-aware call sites and through
|
||||||
|
the dialog's task-id fallback.
|
||||||
|
- Preserve the existing bulk cleanup model, deletion callbacks, layout, focus,
|
||||||
|
and consent behavior.
|
||||||
|
- Update English, Portuguese, Simplified Chinese, Traditional Chinese, and
|
||||||
|
Japanese task catalogs.
|
||||||
|
- Add summary and rendered-dialog regression coverage.
|
||||||
|
|
||||||
|
### Out of scope
|
||||||
|
|
||||||
|
- Backend cleanup ownership, reference counting, or worktree deletion logic.
|
||||||
|
- Bulk delete copy or bulk workspace-mode inference.
|
||||||
|
- A new confirmation surface or responsive layout change.
|
||||||
|
- Public documentation.
|
||||||
|
|
||||||
|
## Technical approach
|
||||||
|
|
||||||
|
1. Extend `getCleanupSummary` with an optional `CleanupSummaryOptions` object.
|
||||||
|
The inherited-parent branch returns the existing session-stop translation
|
||||||
|
and a new parent-workspace note before the executor-specific branch runs.
|
||||||
|
2. Add `sharesParentWorkspace` to `TaskDeleteConfirmDialogProps`. For single
|
||||||
|
deletes, use the prop when present and otherwise resolve the task with
|
||||||
|
`findTaskInSnapshots(taskId, state.kanbanMulti.snapshots, state.kanban.tasks)`
|
||||||
|
through `useAppStore`. Bulk deletes continue using grouped executor copy.
|
||||||
|
3. Pass the task's workspace mode from the five specified callers. Other
|
||||||
|
callers remain protected by the dialog fallback when they provide a task ID.
|
||||||
|
4. Add the new key to all six task catalogs. Run the existing Traditional
|
||||||
|
Chinese generator after updating `zh-cn` and verify catalog parity.
|
||||||
|
5. Add failing summary and dialog tests, then implement the smallest production
|
||||||
|
change and rerun the focused tests before the full task checks.
|
||||||
|
|
||||||
|
## ASCII UI preview
|
||||||
|
|
||||||
|
### UI-01: Inherited-parent workspace delete confirmation
|
||||||
|
|
||||||
|
Entry point: delete action for a child task with a worktree executor and
|
||||||
|
`workspaceMode = inherit_parent`. Desktop and phone use the existing centered
|
||||||
|
`AlertDialog`; on phones the existing body scrolls while the footer remains
|
||||||
|
reachable. The copy hierarchy is the same at both sizes.
|
||||||
|
|
||||||
|
Current behavior:
|
||||||
|
|
||||||
|
```text
|
||||||
|
+----------------------------------------------+
|
||||||
|
| Delete task |
|
||||||
|
| Delete "Child task". This action cannot be |
|
||||||
|
| undone. |
|
||||||
|
| |
|
||||||
|
| Cleanup |
|
||||||
|
| * The task's git worktree and its branch |
|
||||||
|
| will be deleted. |
|
||||||
|
| Your main repo and other branches are not |
|
||||||
|
| affected. |
|
||||||
|
| * Any running agent sessions will be stopped.|
|
||||||
|
| Cancel Delete |
|
||||||
|
+----------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
Corrected behavior:
|
||||||
|
|
||||||
|
```text
|
||||||
|
+----------------------------------------------+
|
||||||
|
| Delete task |
|
||||||
|
| Delete "Child task". This action cannot be |
|
||||||
|
| undone. |
|
||||||
|
| |
|
||||||
|
| Cleanup |
|
||||||
|
| * Any running agent sessions will be stopped.|
|
||||||
|
| This task shares its parent's workspace. The |
|
||||||
|
| parent task's worktree, branch, and files |
|
||||||
|
| are not touched. |
|
||||||
|
| Cancel Delete |
|
||||||
|
+----------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
Structural requirements: keep the named delete outcome, one effects list, one
|
||||||
|
supporting note, existing focus and consent behavior, and existing action
|
||||||
|
reachability. ASCII spacing is illustrative, not a pixel contract. The
|
||||||
|
corrected view covers `AC-UI-TASK-CLEANUP-CONFIRMATION-001.15`.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
| Acceptance criterion | Evidence |
|
||||||
|
| --- | --- |
|
||||||
|
| `AC-UI-TASK-CLEANUP-CONFIRMATION-001.15` | `apps/web/components/task/task-cleanup-summary.test.ts` verifies the exact structured branch; `apps/web/components/task/task-delete-confirm-dialog.test.tsx` verifies rendered parent-worktree preservation and task-id fallback. |
|
||||||
|
| `AC-TASKS-RUNTIME-CLEANUP-001.3` | The same rendered copy regression reflects the existing shared-environment cleanup contract. |
|
||||||
|
|
||||||
|
## E2E tests
|
||||||
|
|
||||||
|
No new Playwright test is required. This is a content and state-normalization
|
||||||
|
change inside the existing confirmation surface. The mobile composition,
|
||||||
|
interaction, and geometry remain unchanged; the focused rendered component
|
||||||
|
test covers the new branch, and the existing mobile confirmation contract
|
||||||
|
continues to apply.
|
||||||
|
|
||||||
|
## Work orders
|
||||||
|
|
||||||
|
- [x] [Task 01: Correct inherited workspace delete confirmation](task-01-inherited-workspace-delete-copy.md)
|
||||||
|
|
||||||
|
## Verification results
|
||||||
|
|
||||||
|
- Focused summary, dialog, and task-menu tests passed: 56 tests across 3 files.
|
||||||
|
- `pnpm run typecheck`, `pnpm run lint`, `pnpm run build:vite`, and `git diff --check` passed.
|
||||||
|
- The scoped Traditional Chinese task catalog generator passed.
|
||||||
|
- `pnpm run i18n:check` remains blocked by 32 pre-existing missing Japanese keys
|
||||||
|
in `executors` and `task`; the new inherited-workspace key is present in all
|
||||||
|
six supported catalogs.
|
||||||
|
- The full Traditional Chinese generator remains blocked by two pre-existing
|
||||||
|
simplified Chinese values in `zh-hk/workflows` and `zh-tw/workflows`.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- A missing task in the store must keep the current executor-based fallback so
|
||||||
|
unrelated dialogs do not lose cleanup information.
|
||||||
|
- Bulk delete must continue using executor grouping and must not infer one
|
||||||
|
workspace mode for several tasks.
|
||||||
|
- The repository currently reports unrelated missing Japanese catalog keys in
|
||||||
|
`pnpm run i18n:check`; the final result must distinguish that baseline issue
|
||||||
|
from any key introduced by this change.
|
||||||
+130
@@ -0,0 +1,130 @@
|
|||||||
|
---
|
||||||
|
id: "01-inherited-workspace-delete-copy"
|
||||||
|
title: "Correct inherited workspace delete confirmation"
|
||||||
|
status: complete
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
plan: "plan.md"
|
||||||
|
requirements:
|
||||||
|
- REQ-UI-TASK-CLEANUP-CONFIRMATION-001
|
||||||
|
- REQ-TASKS-RUNTIME-CLEANUP-001
|
||||||
|
acceptance_criteria:
|
||||||
|
- AC-UI-TASK-CLEANUP-CONFIRMATION-001.15
|
||||||
|
- AC-TASKS-RUNTIME-CLEANUP-001.3
|
||||||
|
system_design:
|
||||||
|
- ../../specs/ui/system-design/confirmation-warning-hierarchy.md
|
||||||
|
- ../../specs/tasks/system-design/runtime-cleanup.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Task 01: Correct inherited workspace delete confirmation
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Make single-task delete confirmation accurately describe a child task that
|
||||||
|
shares its parent's materialized workspace. The summary will show only the
|
||||||
|
agent-session stop effect plus the parent-workspace preservation note, with
|
||||||
|
explicit caller context and a task-store fallback for callers that provide only
|
||||||
|
the task ID.
|
||||||
|
|
||||||
|
## In scope
|
||||||
|
|
||||||
|
- Extend the single-task cleanup summary options and inherited-parent branch.
|
||||||
|
- Add the dialog prop, store lookup fallback, and five specified call-site
|
||||||
|
values.
|
||||||
|
- Add the English and five translated catalog values.
|
||||||
|
- Add summary and dialog regressions, including explicit prop and fallback
|
||||||
|
behavior.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- Backend task cleanup or worktree lifecycle behavior.
|
||||||
|
- Bulk cleanup summary changes.
|
||||||
|
- Confirmation layout, touch sizing, focus, or callback changes.
|
||||||
|
- New Playwright coverage for an unchanged mobile composition.
|
||||||
|
|
||||||
|
## Acceptance
|
||||||
|
|
||||||
|
- `getCleanupSummary(_, { sharesParentWorkspace: true })` returns exactly one
|
||||||
|
agent-session effect and exactly one inherited-parent workspace note, with no
|
||||||
|
worktree or branch deletion copy.
|
||||||
|
- A rendered delete dialog shows the preserved parent worktree, branch, and
|
||||||
|
files when the explicit prop is true or when the task ID resolves to
|
||||||
|
`inherit_parent` in the app store.
|
||||||
|
- Existing executor-specific single and bulk copy, deletion callbacks, and
|
||||||
|
consent behavior remain unchanged for all other tasks.
|
||||||
|
|
||||||
|
## ASCII UI preview
|
||||||
|
|
||||||
|
See [UI-01 in the plan](plan.md#ui-01-inherited-parent-workspace-delete-confirmation).
|
||||||
|
The dialog keeps the existing desktop and phone composition; only the cleanup
|
||||||
|
effects and supporting note change for inherited-parent tasks.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cleanup
|
||||||
|
* Any running agent sessions will be stopped.
|
||||||
|
This task shares its parent's workspace. The parent task's worktree,
|
||||||
|
branch, and files are not touched.
|
||||||
|
|
||||||
|
Cancel Delete
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
(cd apps/web && pnpm run i18n:zh-hant)
|
||||||
|
(cd apps/web && pnpm test components/task/task-cleanup-summary.test.ts components/task/task-delete-confirm-dialog.test.tsx)
|
||||||
|
(cd apps/web && pnpm run typecheck)
|
||||||
|
(cd apps/web && pnpm run i18n:check)
|
||||||
|
(cd apps/web && pnpm run lint)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files likely touched
|
||||||
|
|
||||||
|
- `apps/web/components/task/task-cleanup-summary.ts`
|
||||||
|
- `apps/web/components/task/task-cleanup-summary.test.ts`
|
||||||
|
- `apps/web/components/task/task-delete-confirm-dialog.tsx`
|
||||||
|
- `apps/web/components/task/task-delete-confirm-dialog.test.tsx`
|
||||||
|
- `apps/web/components/task/task-actions-menu-dialogs.tsx`
|
||||||
|
- `apps/web/components/kanban-card-menu.tsx`
|
||||||
|
- `apps/web/components/task/task-management-surface.tsx`
|
||||||
|
- `apps/web/app/tasks/columns.tsx`
|
||||||
|
- `apps/web/app/tasks/tasks-list-view.tsx`
|
||||||
|
- `apps/web/src/locales/{en,pt-pt,zh-cn,zh-hk,zh-tw,ja}/task.json`
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- Callers with stale or missing task projections must retain the generic or
|
||||||
|
executor-based summary through the dialog fallback.
|
||||||
|
- Traditional Chinese catalogs are generator-managed and must be updated with
|
||||||
|
`pnpm run i18n:zh-hant`.
|
||||||
|
- The repository's existing Japanese catalog gaps may keep the full i18n check
|
||||||
|
red after this task; report the exact pre-existing failures.
|
||||||
|
|
||||||
|
## Parallelism
|
||||||
|
|
||||||
|
`sequential`
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
- `docs/specs/ui/requirements/confirmation-warning-hierarchy.md`, especially
|
||||||
|
`AC-UI-TASK-CLEANUP-CONFIRMATION-001.15`.
|
||||||
|
- `docs/specs/ui/system-design/confirmation-warning-hierarchy.md`.
|
||||||
|
- `docs/specs/tasks/requirements/runtime-cleanup.md`,
|
||||||
|
`AC-TASKS-RUNTIME-CLEANUP-001.3`.
|
||||||
|
- `docs/specs/tasks/system-design/runtime-cleanup.md`.
|
||||||
|
- `apps/web/AGENTS.md`.
|
||||||
|
- `.agents/skills/tdd/SKILL.md` and `.agents/skills/mobile-parity/SKILL.md`.
|
||||||
|
|
||||||
|
## Results
|
||||||
|
|
||||||
|
Implemented the inherited-parent cleanup branch, explicit task-aware caller
|
||||||
|
context, task-store fallback, supported locale values, and regression coverage.
|
||||||
|
The focused test files pass with 56 tests. Typecheck, lint, Vite build, and
|
||||||
|
diff validation pass. The scoped Traditional Chinese task catalog generation
|
||||||
|
passes. The full i18n check still reports the 32 pre-existing Japanese missing
|
||||||
|
keys, and the full Traditional Chinese generator still reports two pre-existing
|
||||||
|
simplified Chinese values in `zh-hk/workflows` and `zh-tw/workflows`.
|
||||||
@@ -163,6 +163,11 @@ repository resources, so that I can understand the consequence before acting.
|
|||||||
- **AC-UI-TASK-CLEANUP-CONFIRMATION-001.14:** At phone widths, the discard
|
- **AC-UI-TASK-CLEANUP-CONFIRMATION-001.14:** At phone widths, the discard
|
||||||
selection shall remain inside the existing centered dialog and its scrolling
|
selection shall remain inside the existing centered dialog and its scrolling
|
||||||
body. Its label shall provide a touch target of at least 44 CSS px.
|
body. Its label shall provide a touch target of at least 44 CSS px.
|
||||||
|
- **AC-UI-TASK-CLEANUP-CONFIRMATION-001.15:** When a task uses an inherited
|
||||||
|
parent workspace, its delete confirmation shall state that any running agent
|
||||||
|
sessions for the task will be stopped and that the parent task's worktree,
|
||||||
|
branch, and files remain untouched. It shall omit statements that the task's
|
||||||
|
worktree or branch will be deleted.
|
||||||
|
|
||||||
Consent inspection, clean-workspace suppression, and failure recovery follow
|
Consent inspection, clean-workspace suppression, and failure recovery follow
|
||||||
[task runtime cleanup](../../tasks/requirements/runtime-cleanup.md),
|
[task runtime cleanup](../../tasks/requirements/runtime-cleanup.md),
|
||||||
|
|||||||
@@ -68,6 +68,16 @@ archive popover and inline confirmation use the same ordered model with their
|
|||||||
established compact spacing. This removes copy drift without moving cleanup
|
established compact spacing. This removes copy drift without moving cleanup
|
||||||
policy into a visual component.
|
policy into a visual component.
|
||||||
|
|
||||||
|
Single-task delete also accepts an inherited-parent workspace context. When
|
||||||
|
that context is true, `getCleanupSummary` returns only the generic agent-session
|
||||||
|
effect and the localized note that the parent task's worktree, branch, and files
|
||||||
|
remain untouched. It does not return the worktree deletion effect or its
|
||||||
|
single-worktree reassurance. `TaskDeleteConfirmDialog` receives this context
|
||||||
|
from task-aware callers and, when a caller provides only `taskId`, resolves
|
||||||
|
`workspaceMode` from `kanbanMulti.snapshots` with `kanban.tasks` as the fallback.
|
||||||
|
If the task cannot be resolved, the existing executor-based summary remains the
|
||||||
|
safe fallback.
|
||||||
|
|
||||||
### Full-dialog composition
|
### Full-dialog composition
|
||||||
|
|
||||||
`TaskDeleteConfirmDialog` keeps Radix `AlertDialog` and the existing centered
|
`TaskDeleteConfirmDialog` keeps Radix `AlertDialog` and the existing centered
|
||||||
@@ -253,6 +263,8 @@ disabled actions, single and bulk callback values, cascade behavior, and typed
|
|||||||
conflict feedback. Dialog and compact-surface tests assert equivalent structured
|
conflict feedback. Dialog and compact-surface tests assert equivalent structured
|
||||||
content, semantic action variants, unchanged callbacks, and that a deferred
|
content, semantic action variants, unchanged callbacks, and that a deferred
|
||||||
positive descendant result does not expose a provisional fine-pointer popover.
|
positive descendant result does not expose a provisional fine-pointer popover.
|
||||||
|
The inherited-parent cleanup path is covered at both the summary and rendered
|
||||||
|
dialog boundaries, including the task-id resolution fallback.
|
||||||
Pending-dismissal component cases cover Escape, trigger focus restoration, and
|
Pending-dismissal component cases cover Escape, trigger focus restoration, and
|
||||||
outside pointer intent.
|
outside pointer intent.
|
||||||
Rendered desktop and phone checks cover viewport bounds, scroll ownership,
|
Rendered desktop and phone checks cover viewport bounds, scroll ownership,
|
||||||
|
|||||||
Reference in New Issue
Block a user