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 { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
|
||||
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 { TaskArchiveConfirmation } from "@/components/task/task-archive-confirmation";
|
||||
import { linkToTask } from "@/lib/links";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { t } from "@/lib/i18n";
|
||||
import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
|
||||
import { workspaceModeFromMetadata } from "@/lib/kanban/map-task";
|
||||
|
||||
type TaskWithResolution = Task & {
|
||||
workflowName?: string;
|
||||
@@ -132,6 +134,9 @@ function ActionsCell({ row, ctx }: { row: Row<TaskWithResolution>; ctx: ActionsC
|
||||
taskTitle={task.title}
|
||||
taskId={task.id}
|
||||
executorType={task.primary_executor_type}
|
||||
sharesParentWorkspace={cleanupSharesParentWorkspace(
|
||||
workspaceModeFromMetadata(task.metadata),
|
||||
)}
|
||||
isDeleting={isDeleting}
|
||||
onConfirm={({ 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 { IconArchive, IconArchiveOff, IconLoader, IconTrash } from "@tabler/icons-react";
|
||||
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 { primaryTaskRepository, type Repository, type Task, type Workflow } from "@/lib/types/http";
|
||||
import { formatTaskStateLabel } from "@/lib/ui/state-labels";
|
||||
@@ -20,6 +21,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { t } from "@/lib/i18n";
|
||||
import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
|
||||
import type { TaskListFacetValue } from "@/lib/plugins/types";
|
||||
import { workspaceModeFromMetadata } from "@/lib/kanban/map-task";
|
||||
|
||||
export type TasksListViewProps = {
|
||||
total: number;
|
||||
@@ -548,6 +550,7 @@ function TaskRowActions({
|
||||
) => Promise<void>;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const workspaceMode = workspaceModeFromMetadata(task.metadata);
|
||||
const archiveAnchorRef = useRef<HTMLButtonElement>(null);
|
||||
const { isFinePointer } = useResponsiveBreakpoint();
|
||||
return (
|
||||
@@ -601,6 +604,7 @@ function TaskRowActions({
|
||||
taskId={task.id}
|
||||
isInFlight={isTaskInFlight(task.foreground_activity)}
|
||||
executorType={task.primary_executor_type}
|
||||
sharesParentWorkspace={cleanupSharesParentWorkspace(workspaceMode)}
|
||||
isDeleting={isDeleting}
|
||||
onConfirm={({ cascade, discardWorktreeChanges }) =>
|
||||
onDelete(task.id, { cascade, discardWorktreeChanges })
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
useKanbanCardMoveTargets,
|
||||
} from "@/components/kanban-card-menu-items";
|
||||
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 {
|
||||
TaskExternalLinkDialog,
|
||||
@@ -269,6 +270,7 @@ export function KanbanCardDialogs({
|
||||
taskTitle={task.title}
|
||||
taskId={task.id}
|
||||
executorType={task.primaryExecutorType}
|
||||
sharesParentWorkspace={cleanupSharesParentWorkspace(task.workspaceMode)}
|
||||
isDeleting={isDeleting}
|
||||
onConfirm={(opts) => onDelete?.(task, opts)}
|
||||
/>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useRef, type RefObject } from "react";
|
||||
import { useActiveWorkspaceRepositories } from "@/components/kanban-card-repositories";
|
||||
import { TaskArchiveConfirmation } from "@/components/task/task-archive-confirmation";
|
||||
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 { TaskDetachConfirmationSurface } from "@/components/task/task-detach-confirm-dialog";
|
||||
import { TaskExternalLinkDialog } from "@/components/task/task-external-link-dialog";
|
||||
@@ -122,6 +123,20 @@ function useLastResolvedBoardRow(
|
||||
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
|
||||
* so a dialog outlives the menu that opened it.
|
||||
@@ -140,13 +155,7 @@ export function TaskActionsMenuDialogs({
|
||||
const editBoardRow = useLastResolvedBoardRow(taskId, boardRow);
|
||||
if (!taskId) return null;
|
||||
const linkDialogTask = buildLinkDialogTask(taskId, taskTitle, boardRow);
|
||||
const confirmationTarget = {
|
||||
taskId,
|
||||
taskTitle,
|
||||
anchorRef: menu.triggerRef,
|
||||
focusReturnRef: menu.triggerRef,
|
||||
restoreFocusOnConfirm: true,
|
||||
};
|
||||
const confirmationTarget = buildConfirmationTarget(taskId, taskTitle, menu.triggerRef);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -175,6 +184,7 @@ export function TaskActionsMenuDialogs({
|
||||
taskTitle={taskTitle}
|
||||
taskId={taskId}
|
||||
executorType={boardRow?.primaryExecutorType ?? subjectExecutorType}
|
||||
sharesParentWorkspace={cleanupSharesParentWorkspace(boardRow?.workspaceMode)}
|
||||
isDeleting={isDeleting}
|
||||
onConfirm={(opts) => menu.onConfirmDelete(opts)}
|
||||
focusReturnRef={menu.triggerRef}
|
||||
|
||||
@@ -25,6 +25,15 @@ describe("getCleanupSummary (single task)", () => {
|
||||
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", () => {
|
||||
const { effects, notes } = getCleanupSummary("local_docker");
|
||||
expect(effects[0]).toMatch(/Docker container/i);
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
// Executor types match models.ExecutorType in apps/backend/internal/task/models/models.go.
|
||||
import { t } from "@/lib/i18n";
|
||||
import type { WorkspaceMode } from "@/lib/kanban/map-task";
|
||||
|
||||
export type CleanupSummary = {
|
||||
effects: 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.
|
||||
type KnownExecutor =
|
||||
| "local"
|
||||
@@ -45,6 +56,7 @@ const SINGLE_COPIES: Record<KnownExecutor, CleanupCopy> = {
|
||||
};
|
||||
|
||||
const GENERIC_EFFECT_KEY = "task:cleanupAgentSessionsStopped";
|
||||
const INHERITED_PARENT_WORKSPACE_NOTE_KEY = "task:cleanupInheritedParentWorkspaceNote";
|
||||
|
||||
function normalize(executorType: string | null | undefined): KnownExecutor | null {
|
||||
if (!executorType) return null;
|
||||
@@ -65,7 +77,16 @@ function resolveCopy(copy: CleanupCopy, options?: { count: number }): CleanupSum
|
||||
}
|
||||
|
||||
/** 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);
|
||||
return known
|
||||
? resolveCopy(SINGLE_COPIES[known])
|
||||
|
||||
@@ -14,7 +14,11 @@ vi.mock("@/lib/api", () => ({
|
||||
import { TaskDeleteConfirmDialog } from "./task-delete-confirm-dialog";
|
||||
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,
|
||||
// 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,
|
||||
position: 0,
|
||||
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", () => {
|
||||
it("keeps the shared in-flight warning visually subordinate", () => {
|
||||
mockGetSubtaskCount.mockResolvedValue({ count: 0 });
|
||||
|
||||
@@ -23,6 +23,8 @@ import { useTaskInFlight } from "@/hooks/use-task-in-flight";
|
||||
import { getCleanupSummary, getBulkCleanupSummary } from "./task-cleanup-summary";
|
||||
import { TaskCleanupConsequences } from "./task-cleanup-consequences";
|
||||
import { StillWorkingWarning } from "./task-still-working-warning";
|
||||
import { useAppStore } from "@/components/state-provider";
|
||||
import { findTaskInSnapshots } from "@/lib/kanban/find-task";
|
||||
import {
|
||||
TASK_CONFIRM_ACTION_CLASS,
|
||||
TASK_CONFIRM_BODY_CLASS,
|
||||
@@ -34,6 +36,14 @@ import {
|
||||
import { useTranslation } from "react-i18next";
|
||||
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 = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
@@ -48,6 +58,8 @@ type TaskDeleteConfirmDialogProps = {
|
||||
executorType?: string | null;
|
||||
/** Executor types of the tasks being deleted (bulk). */
|
||||
executorTypes?: Array<string | null | undefined>;
|
||||
/** Whether the single task borrows its workspace from its parent. */
|
||||
sharesParentWorkspace?: boolean;
|
||||
onConfirm: (opts: { cascade: boolean; discardWorktreeChanges: boolean }) => void;
|
||||
confirmTestId?: string;
|
||||
/** Overrides default focus restoration when the original trigger may disappear. */
|
||||
@@ -284,6 +296,7 @@ export function TaskDeleteConfirmDialog({
|
||||
isInFlight,
|
||||
executorType,
|
||||
executorTypes,
|
||||
sharesParentWorkspace,
|
||||
onConfirm,
|
||||
confirmTestId,
|
||||
onCloseAutoFocus,
|
||||
@@ -297,9 +310,12 @@ export function TaskDeleteConfirmDialog({
|
||||
const description = isBulkOperation
|
||||
? t("task:deleteTasksConfirm", { count: safeCount })
|
||||
: t("task:deleteTaskConfirm", { taskTitle });
|
||||
const storeSharesParentWorkspace = useStoreSharesParentWorkspace(taskId, isBulkOperation);
|
||||
const cleanup = isBulkOperation
|
||||
? getBulkCleanupSummary(executorTypes ?? [])
|
||||
: getCleanupSummary(executorType);
|
||||
: getCleanupSummary(executorType, {
|
||||
sharesParentWorkspace: storeSharesParentWorkspace ?? sharesParentWorkspace,
|
||||
});
|
||||
|
||||
const {
|
||||
cascade,
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { TaskPriority } from "@/lib/types/http";
|
||||
import { TaskManagementMenu, type TaskManagementMenuProps } from "./task-management-menu";
|
||||
import { TaskManagementDrawer, TaskManagementSheet } from "./task-management-drawer";
|
||||
import { TaskArchiveConfirmation } from "./task-archive-confirmation";
|
||||
import { cleanupSharesParentWorkspace } from "./task-cleanup-summary";
|
||||
import { TaskDeleteConfirmDialog } from "./task-delete-confirm-dialog";
|
||||
import { useSidebarLinkActions } from "./task-session-sidebar-link-actions";
|
||||
import { useSidebarTaskLinking } from "./task-session-sidebar-task-linking";
|
||||
@@ -215,6 +216,7 @@ function ManagementConfirmations({
|
||||
taskId={task.id}
|
||||
taskTitle={task.title}
|
||||
executorType={task.remoteExecutorType}
|
||||
sharesParentWorkspace={cleanupSharesParentWorkspace(task.workspaceMode)}
|
||||
isDeleting={mutations.disabled}
|
||||
onCloseAutoFocus={onCloseAutoFocus}
|
||||
confirmTestId="thread-delete-confirm"
|
||||
|
||||
@@ -3,7 +3,10 @@ import { createAppStore } from "@/lib/state/store";
|
||||
import { resolveTaskMenuTarget } from "./task-menu-target";
|
||||
|
||||
const identity = { taskId: "A", workspaceId: "workspace" };
|
||||
function fixture(loss?: string) {
|
||||
function fixture(
|
||||
loss?: string,
|
||||
workspaceMode?: "inherit_parent" | "new_workspace" | "shared_group",
|
||||
) {
|
||||
const store = createAppStore();
|
||||
store.setState((state) => ({
|
||||
workspaces: { ...state.workspaces, activeId: loss === "workspace" ? "elsewhere" : "workspace" },
|
||||
@@ -32,6 +35,7 @@ function fixture(loss?: string) {
|
||||
priority: "high",
|
||||
isArchived: loss === "archived",
|
||||
primaryExecutorType: "worktree",
|
||||
workspaceMode,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -61,4 +65,9 @@ describe("task menu eligibility", () => {
|
||||
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,
|
||||
primarySessionId: task.primarySessionId,
|
||||
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.",
|
||||
"cleanupSingleSsh": "The task directory on the remote host will be removed.",
|
||||
"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.",
|
||||
"cleanupSingleWorktreeNote": "Your main repo and other branches are not affected.",
|
||||
"enhancedPromptCopiedToClipboard": "Enhanced prompt copied to clipboard.",
|
||||
|
||||
@@ -2237,6 +2237,7 @@
|
||||
"cleanupSingleSprites": "このタスクの Sprites クラウドサンドボックスは破棄されます。中の未コミット作業は失われます。",
|
||||
"cleanupSingleSsh": "リモートホスト上のタスクディレクトリが削除されます。",
|
||||
"cleanupSingleSshNote": "ベストエフォート削除なので、完了しない場合があります。ローカルリポジトリには触れません。",
|
||||
"cleanupInheritedParentWorkspaceNote": "このタスクは親タスクのワークスペースを共有します。親タスクの worktree、ブランチ、ファイルには触れません。",
|
||||
"cleanupSingleWorktree": "タスクの git worktree とそのブランチが削除されます。",
|
||||
"cleanupSingleWorktreeNote": "メインリポジトリと他のブランチには影響しません。",
|
||||
"enhancedPromptCopiedToClipboard": "拡張プロンプトをクリップボードにコピーしました。",
|
||||
|
||||
@@ -2237,6 +2237,7 @@
|
||||
"cleanupSingleSprites": "Ţĥē Śƥŕĩţēś ćĺōũď śàńďƀōx ƒōŕ ţĥĩś ţàśķ ŵĩĺĺ ƀē ďēśţŕōŷēď. Àńŷ ũńćōḿḿĩţţēď ŵōŕķ ĩńśĩďē ĩţ ŵĩĺĺ ƀē ĺōśţ.",
|
||||
"cleanupSingleSsh": "Ţĥē ţàśķ ďĩŕēćţōŕŷ ōń ţĥē ŕēḿōţē ĥōśţ ŵĩĺĺ ƀē ŕēḿōvēď.",
|
||||
"cleanupSingleSshNote": "Ɓēśţ-ēƒƒōŕţ ŕēḿōvàĺ ḿàŷ ńōţ ćōḿƥĺēţē. Ŷōũŕ ĺōćàĺ ŕēƥō ĩś ńōţ ţōũćĥēď.",
|
||||
"cleanupInheritedParentWorkspaceNote": "Ţĥĩś ţàśķ śĥàŕēś ĩţś ƥàŕēńţ'ś ŵōŕķśƥàćē. Ţĥē ƥàŕēńţ ţàśķ'ś ŵōŕķţŕēē, ƀŕàńćĥ, àńď ƒĩĺēś àŕē ńōţ ţōũćĥēď.",
|
||||
"cleanupSingleWorktree": "Ţĥē ţàśķ'ś ĝĩţ ŵōŕķţŕēē àńď ĩţś ƀŕàńćĥ ŵĩĺĺ ƀē ďēĺēţēď.",
|
||||
"cleanupSingleWorktreeNote": "Ŷōũŕ ḿàĩń ŕēƥō àńď ōţĥēŕ ƀŕàńćĥēś àŕē ńōţ àƒƒēćţēď.",
|
||||
"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.",
|
||||
"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.",
|
||||
"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.",
|
||||
"cleanupSingleWorktreeNote": "O seu repositório principal e os outros ramos não são afetados.",
|
||||
"composerPlaceholderBusy": "Colocar instruções na fila para o agente...",
|
||||
|
||||
@@ -2101,6 +2101,7 @@
|
||||
"cleanupSingleSprites": "该任务的 Sprites 云沙箱将被销毁。其中所有未提交的工作都会丢失。",
|
||||
"cleanupSingleSsh": "远程主机上的任务目录将被移除。",
|
||||
"cleanupSingleSshNote": "尽力移除,可能无法完成。你的本地仓库不会被改动。",
|
||||
"cleanupInheritedParentWorkspaceNote": "此任务与父任务共享工作区。父任务的 worktree、分支和文件不会受到影响。",
|
||||
"cleanupSingleWorktree": "该任务的 git worktree 及其分支将被删除。",
|
||||
"cleanupSingleWorktreeNote": "你的主仓库和其他分支不受影响。",
|
||||
"composerPlaceholderBusy": "为智能体排队指令…",
|
||||
|
||||
@@ -2101,6 +2101,7 @@
|
||||
"cleanupSingleSprites": "該任務的 Sprites 雲沙箱將被銷燬。其中所有未提交的工作都會丟失。",
|
||||
"cleanupSingleSsh": "遠端主機上的任務目錄將被移除。",
|
||||
"cleanupSingleSshNote": "盡力移除,可能無法完成。你的本地儲存庫不會被改動。",
|
||||
"cleanupInheritedParentWorkspaceNote": "此任務與父任務共享工作區。父任務的 worktree、分支和檔案不會受到影響。",
|
||||
"cleanupSingleWorktree": "該任務的 git worktree 及其分支將被刪除。",
|
||||
"cleanupSingleWorktreeNote": "你的主儲存庫和其他分支不受影響。",
|
||||
"composerPlaceholderBusy": "為代理程式排隊指令…",
|
||||
|
||||
@@ -2101,6 +2101,7 @@
|
||||
"cleanupSingleSprites": "該任務的 Sprites 雲沙箱將被銷燬。其中所有未提交的工作都會丟失。",
|
||||
"cleanupSingleSsh": "遠端主機上的任務目錄將被移除。",
|
||||
"cleanupSingleSshNote": "盡力移除,可能無法完成。你的本地儲存庫不會被改動。",
|
||||
"cleanupInheritedParentWorkspaceNote": "此任務與父任務共享工作區。父任務的 worktree、分支和檔案不會受到影響。",
|
||||
"cleanupSingleWorktree": "該任務的 git worktree 及其分支將被刪除。",
|
||||
"cleanupSingleWorktreeNote": "你的主儲存庫和其他分支不受影響。",
|
||||
"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
|
||||
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.
|
||||
- **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
|
||||
[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
|
||||
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
|
||||
|
||||
`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
|
||||
content, semantic action variants, unchanged callbacks, and that a deferred
|
||||
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
|
||||
outside pointer intent.
|
||||
Rendered desktop and phone checks cover viewport bounds, scroll ownership,
|
||||
|
||||
Reference in New Issue
Block a user