fix(web): refine task cleanup confirmations (#3231)
This commit is contained in:
@@ -79,7 +79,9 @@ describe("PRMergedBanner", () => {
|
||||
fireEvent.click(screen.getByTestId(MERGED_ARCHIVE_BUTTON));
|
||||
|
||||
expect(await screen.findByTestId(MERGED_ARCHIVE_CONFIRM)).toBeTruthy();
|
||||
expect(screen.getByText(/Are you sure you want to archive "Task One"\?/)).toBeTruthy();
|
||||
expect(screen.getByTestId("task-confirmation-outcome").textContent).toMatch(
|
||||
/Archive [“"]?Task One[”"]?\./i,
|
||||
);
|
||||
expect(archiveAndSwitchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -168,7 +170,9 @@ describe("PRMergedBanner", () => {
|
||||
fireEvent.click(screen.getByTestId(MERGED_ARCHIVE_BUTTON));
|
||||
|
||||
expect(await screen.findByTestId(MERGED_ARCHIVE_CONFIRM)).toBeTruthy();
|
||||
expect(screen.getByText(/Are you sure you want to archive "this task"\?/)).toBeTruthy();
|
||||
expect(screen.getByTestId("task-confirmation-outcome").textContent).toMatch(
|
||||
/Archive [“"]?this task[”"]?\./i,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { defaultState } from "@/lib/state/default-state";
|
||||
import { expectCompactWarning } from "./task-confirm-dialog.test-helpers";
|
||||
|
||||
const mockGetSubtaskCount = vi.fn();
|
||||
const ARCHIVE_CONFIRM_TEST_ID = "archive-confirm";
|
||||
|
||||
vi.mock("@/lib/api", () => ({
|
||||
getSubtaskCount: (...args: unknown[]) => mockGetSubtaskCount(...args),
|
||||
@@ -77,6 +78,36 @@ beforeEach(() => {
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
describe("TaskArchiveConfirmDialog presentation", () => {
|
||||
it("contains long confirmation content in a scrolling body with touch-safe actions", () => {
|
||||
renderDialog(
|
||||
<TaskArchiveConfirmDialog
|
||||
open
|
||||
onOpenChange={() => {}}
|
||||
taskTitle="A task with a title that needs to wrap inside a phone confirmation surface"
|
||||
taskId="task-1"
|
||||
executorType="sprites"
|
||||
isInFlight
|
||||
confirmTestId={ARCHIVE_CONFIRM_TEST_ID}
|
||||
onConfirm={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const dialog = screen.getByRole("alertdialog");
|
||||
expect(dialog.className).toContain("max-h-[calc(100dvh-2rem)]");
|
||||
expect(dialog.className).toContain("grid-rows-[auto_minmax(0,1fr)_auto]");
|
||||
expect(dialog.className).toContain("overflow-hidden");
|
||||
expect(screen.getByTestId("task-confirmation-body").className).toContain("min-h-0");
|
||||
expect(screen.getByTestId("task-confirmation-body").className).toContain("space-y-3");
|
||||
expect(screen.getByTestId("task-confirmation-body").className).toContain("overflow-y-auto");
|
||||
expect(screen.getByTestId(ARCHIVE_CONFIRM_TEST_ID).className).toContain("min-h-11");
|
||||
expect(screen.getByTestId(ARCHIVE_CONFIRM_TEST_ID).className).toContain("w-full");
|
||||
expect(screen.getByTestId(ARCHIVE_CONFIRM_TEST_ID).getAttribute("data-variant")).toBe(
|
||||
"default",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TaskArchiveConfirmDialog preference", () => {
|
||||
it("archives once without rendering a dialog when confirmation is disabled", async () => {
|
||||
const onConfirm = vi.fn();
|
||||
@@ -139,12 +170,12 @@ describe("TaskArchiveConfirmDialog classification safety", () => {
|
||||
taskId="task-1"
|
||||
executorType="worktree"
|
||||
subtaskClassification={{ status: "loading", total: 0 }}
|
||||
confirmTestId="archive-confirm"
|
||||
confirmTestId={ARCHIVE_CONFIRM_TEST_ID}
|
||||
onConfirm={onConfirm}
|
||||
/>,
|
||||
);
|
||||
|
||||
const confirm = screen.getByTestId("archive-confirm");
|
||||
const confirm = screen.getByTestId(ARCHIVE_CONFIRM_TEST_ID);
|
||||
expect(confirm.hasAttribute("disabled")).toBe(true);
|
||||
fireEvent.click(confirm);
|
||||
expect(onConfirm).not.toHaveBeenCalled();
|
||||
@@ -161,16 +192,39 @@ describe("TaskArchiveConfirmDialog classification safety", () => {
|
||||
taskId="task-1"
|
||||
executorType="worktree"
|
||||
subtaskClassification={{ status: "error", total: 0 }}
|
||||
confirmTestId="archive-confirm"
|
||||
confirmTestId={ARCHIVE_CONFIRM_TEST_ID}
|
||||
onConfirm={onConfirm}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByTestId("archive-confirm"));
|
||||
fireEvent.click(screen.getByTestId(ARCHIVE_CONFIRM_TEST_ID));
|
||||
expect(onConfirm).toHaveBeenCalledWith({ cascade: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe("TaskArchiveConfirmDialog direct outcome", () => {
|
||||
it("states the named archive outcome directly and separates cleanup effects", () => {
|
||||
renderDialog(
|
||||
<TaskArchiveConfirmDialog
|
||||
open
|
||||
onOpenChange={() => {}}
|
||||
taskTitle="My task"
|
||||
taskId="task-1"
|
||||
executorType="worktree"
|
||||
onConfirm={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("task-confirmation-outcome").textContent).toMatch(
|
||||
/Archive [“"]?My task[”"]?\./i,
|
||||
);
|
||||
expect(screen.getByTestId("task-cleanup-effects").tagName).toBe("UL");
|
||||
expect(screen.getByTestId("task-cleanup-effects").querySelectorAll("li")).toHaveLength(2);
|
||||
expect(screen.getByTestId("task-cleanup-notes").tagName).toBe("DIV");
|
||||
expect(screen.queryByText(/Are you sure/i)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("TaskArchiveConfirmDialog cleanup copy", () => {
|
||||
it("renders local-executor reassurance about untouched repo", () => {
|
||||
renderDialog(
|
||||
|
||||
@@ -17,9 +17,13 @@ import { useAppStore } from "@/components/state-provider";
|
||||
import { useSubtaskCountState, type SubtaskCountResult } from "@/hooks/use-subtask-count";
|
||||
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 {
|
||||
TASK_CONFIRM_ACTION_CLASS,
|
||||
TASK_CONFIRM_BODY_CLASS,
|
||||
TASK_CONFIRM_CLASS,
|
||||
TASK_CONFIRM_FOOTER_CLASS,
|
||||
TASK_CONFIRM_HEADER_CLASS,
|
||||
stopDialogPropagation,
|
||||
} from "./task-confirm-dialog-shared";
|
||||
@@ -159,41 +163,42 @@ export function TaskArchiveConfirmDialog({
|
||||
<AlertDialogContent size="lg" className={TASK_CONFIRM_CLASS} onClick={stopDialogPropagation}>
|
||||
<AlertDialogHeader className={TASK_CONFIRM_HEADER_CLASS}>
|
||||
<AlertDialogTitle className="text-base font-semibold">{title}</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild className="text-sm leading-6">
|
||||
<div>
|
||||
<p>{firstLine}</p>
|
||||
{cleanup.lines.map((line, i) => (
|
||||
<p key={i} className="mt-2" data-testid="cleanup-line">
|
||||
{line}
|
||||
</p>
|
||||
))}
|
||||
</AlertDialogHeader>
|
||||
<div data-testid="task-confirmation-body" className={TASK_CONFIRM_BODY_CLASS}>
|
||||
<AlertDialogDescription asChild className="text-left text-sm leading-6">
|
||||
<div className="space-y-3">
|
||||
<p data-testid="task-confirmation-outcome">{firstLine}</p>
|
||||
<TaskCleanupConsequences summary={cleanup} />
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
{taskIsInFlight && <StillWorkingWarning count={isBulkOperation ? safeCount : undefined} />}
|
||||
{subtaskCount > 0 && (
|
||||
<label className="flex items-start gap-2 text-sm cursor-pointer">
|
||||
<Checkbox
|
||||
checked={cascade}
|
||||
onCheckedChange={(v) => setCascade(v === true)}
|
||||
disabled={isArchiving}
|
||||
data-testid="archive-cascade-checkbox"
|
||||
/>
|
||||
<span>
|
||||
{t("task:alsoArchiveSubtasks", { count: subtaskCount })}
|
||||
<span className="block text-sm text-muted-foreground">
|
||||
{t("task:subtasksStayActiveUnlessYouTick")}
|
||||
{taskIsInFlight && (
|
||||
<StillWorkingWarning count={isBulkOperation ? safeCount : undefined} />
|
||||
)}
|
||||
{subtaskCount > 0 && (
|
||||
<label className="flex cursor-pointer items-start gap-2 text-sm">
|
||||
<Checkbox
|
||||
checked={cascade}
|
||||
onCheckedChange={(v) => setCascade(v === true)}
|
||||
disabled={isArchiving}
|
||||
data-testid="archive-cascade-checkbox"
|
||||
/>
|
||||
<span>
|
||||
{t("task:alsoArchiveSubtasks", { count: subtaskCount })}
|
||||
<span className="block text-sm text-muted-foreground">
|
||||
{t("task:subtasksStayActiveUnlessYouTick")}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel className="cursor-pointer !text-sm">
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
<AlertDialogFooter className={TASK_CONFIRM_FOOTER_CLASS}>
|
||||
<AlertDialogCancel className={TASK_CONFIRM_ACTION_CLASS}>
|
||||
{t("common:cancel")}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
variant="default"
|
||||
disabled={archiveDisabled}
|
||||
className="cursor-pointer !text-sm"
|
||||
className={TASK_CONFIRM_ACTION_CLASS}
|
||||
data-testid={confirmTestId}
|
||||
onClick={() => {
|
||||
if (archiveDisabled) return;
|
||||
|
||||
@@ -6,6 +6,9 @@ import { StateProvider } from "@/components/state-provider";
|
||||
const getSubtaskCountMock = vi.hoisted(() => vi.fn());
|
||||
const pointerState = vi.hoisted(() => ({ isFinePointer: false }));
|
||||
const CONFIRM_TEST_ID = "archive-task-confirm";
|
||||
const INLINE_CONFIRMATION_TEST_ID = "task-archive-inline-confirmation";
|
||||
const CLEANUP_EFFECTS_TEST_ID = "task-cleanup-effects";
|
||||
const CLEANUP_NOTES_TEST_ID = "task-cleanup-notes";
|
||||
|
||||
vi.mock("@/lib/api", () => ({
|
||||
getSubtaskCount: (...args: unknown[]) => getSubtaskCountMock(...args),
|
||||
@@ -90,12 +93,26 @@ describe("TaskArchiveConfirmation classification", () => {
|
||||
|
||||
renderConfirmation();
|
||||
|
||||
const confirmation = await screen.findByTestId("task-archive-inline-confirmation");
|
||||
const confirmation = await screen.findByTestId(INLINE_CONFIRMATION_TEST_ID);
|
||||
expect(confirmation).toBeTruthy();
|
||||
expect(screen.queryByRole("alertdialog")).toBeNull();
|
||||
expect(screen.getByTestId(CONFIRM_TEST_ID).className).toContain("h-11");
|
||||
});
|
||||
|
||||
it("uses the same semantic cleanup effect list and supporting notes as the dialog", async () => {
|
||||
getSubtaskCountMock.mockResolvedValue({ count: 0 });
|
||||
|
||||
renderConfirmation();
|
||||
|
||||
await screen.findByTestId(INLINE_CONFIRMATION_TEST_ID);
|
||||
expect(screen.getByTestId(CLEANUP_EFFECTS_TEST_ID).getAttribute("role")).toBe("list");
|
||||
expect(
|
||||
screen.getByTestId(CLEANUP_EFFECTS_TEST_ID).querySelectorAll('[role="listitem"]'),
|
||||
).toHaveLength(2);
|
||||
expect(screen.getByTestId(CLEANUP_NOTES_TEST_ID).tagName).toBe("SPAN");
|
||||
expect(screen.queryByText(/Are you sure/i)).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps descendants on the cascade dialog branch", async () => {
|
||||
getSubtaskCountMock.mockResolvedValue({ count: 2 });
|
||||
|
||||
@@ -104,7 +121,7 @@ describe("TaskArchiveConfirmation classification", () => {
|
||||
const dialog = await screen.findByRole("alertdialog");
|
||||
expect(dialog).toBeTruthy();
|
||||
expect(screen.getByTestId("archive-cascade-checkbox")).toBeTruthy();
|
||||
expect(screen.queryByTestId("task-archive-inline-confirmation")).toBeNull();
|
||||
expect(screen.queryByTestId(INLINE_CONFIRMATION_TEST_ID)).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps an unknown classification on the safe dialog branch", async () => {
|
||||
@@ -120,7 +137,7 @@ describe("TaskArchiveConfirmation classification", () => {
|
||||
getSubtaskCountMock.mockResolvedValue({ count: 0 });
|
||||
let closedBeforeConfirm = false;
|
||||
const onConfirm = vi.fn(() => {
|
||||
closedBeforeConfirm = screen.queryByTestId("task-archive-inline-confirmation") === null;
|
||||
closedBeforeConfirm = screen.queryByTestId(INLINE_CONFIRMATION_TEST_ID) === null;
|
||||
});
|
||||
|
||||
renderConfirmation(onConfirm);
|
||||
@@ -130,3 +147,19 @@ describe("TaskArchiveConfirmation classification", () => {
|
||||
expect(closedBeforeConfirm).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TaskArchiveConfirmation cleanup copy", () => {
|
||||
it("uses the same semantic cleanup effect list in the fine-pointer popover", async () => {
|
||||
pointerState.isFinePointer = true;
|
||||
getSubtaskCountMock.mockResolvedValue({ count: 0 });
|
||||
|
||||
renderConfirmation();
|
||||
|
||||
await screen.findByTestId("task-archive-confirm-popover");
|
||||
expect(screen.getByTestId(CLEANUP_EFFECTS_TEST_ID).getAttribute("role")).toBe("list");
|
||||
expect(
|
||||
screen.getByTestId(CLEANUP_EFFECTS_TEST_ID).querySelectorAll('[role="listitem"]'),
|
||||
).toHaveLength(2);
|
||||
expect(screen.getByTestId(CLEANUP_NOTES_TEST_ID).tagName).toBe("SPAN");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,8 @@ import { InlineConfirmActions } from "@/components/confirmation/inline-confirm-a
|
||||
import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint";
|
||||
import { useSubtaskCountState, type SubtaskCountResult } from "@/hooks/use-subtask-count";
|
||||
import { useTaskInFlight } from "@/hooks/use-task-in-flight";
|
||||
import { getCleanupSummary } from "./task-cleanup-summary";
|
||||
import { getCleanupSummary, type CleanupSummary } from "./task-cleanup-summary";
|
||||
import { TaskCleanupConsequences } from "./task-cleanup-consequences";
|
||||
import { StillWorkingWarning } from "./task-still-working-warning";
|
||||
import { TaskArchiveConfirmDialog } from "./task-archive-confirm-dialog";
|
||||
|
||||
@@ -42,22 +43,20 @@ export type TaskArchiveConfirmationProps = {
|
||||
|
||||
function ArchiveDescription({
|
||||
taskTitle,
|
||||
cleanupLines,
|
||||
cleanup,
|
||||
taskIsInFlight,
|
||||
}: {
|
||||
taskTitle?: string;
|
||||
cleanupLines: string[];
|
||||
cleanup: CleanupSummary;
|
||||
taskIsInFlight: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<span className="block space-y-2">
|
||||
<span className="block">{t("task:archiveTaskConfirm", { taskTitle })}</span>
|
||||
{cleanupLines.map((line) => (
|
||||
<span key={line} className="block">
|
||||
{line}
|
||||
</span>
|
||||
))}
|
||||
<span data-testid="task-confirmation-outcome" className="block">
|
||||
{t("task:archiveTaskConfirm", { taskTitle })}
|
||||
</span>
|
||||
<TaskCleanupConsequences summary={cleanup} compact />
|
||||
{taskIsInFlight && <StillWorkingWarning />}
|
||||
</span>
|
||||
);
|
||||
@@ -83,12 +82,9 @@ function ArchiveConfirmCopy({
|
||||
confirmTestId: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const cleanup = getCleanupSummary(executorType);
|
||||
const description = (
|
||||
<ArchiveDescription
|
||||
taskTitle={taskTitle}
|
||||
cleanupLines={getCleanupSummary(executorType).lines}
|
||||
taskIsInFlight={isInFlight}
|
||||
/>
|
||||
<ArchiveDescription taskTitle={taskTitle} cleanup={cleanup} taskIsInFlight={isInFlight} />
|
||||
);
|
||||
return (
|
||||
<InlineConfirmActions
|
||||
@@ -134,6 +130,7 @@ function ArchiveConfirmPopover({
|
||||
confirmTestId: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const cleanup = getCleanupSummary(executorType);
|
||||
return (
|
||||
<ActionConfirmPopover
|
||||
open={open}
|
||||
@@ -144,11 +141,7 @@ function ArchiveConfirmPopover({
|
||||
focusBoundaryRef={focusBoundaryRef ?? anchorRef}
|
||||
title={t("task:archiveTaskTitle")}
|
||||
description={
|
||||
<ArchiveDescription
|
||||
taskTitle={taskTitle}
|
||||
cleanupLines={getCleanupSummary(executorType).lines}
|
||||
taskIsInFlight={isInFlight}
|
||||
/>
|
||||
<ArchiveDescription taskTitle={taskTitle} cleanup={cleanup} taskIsInFlight={isInFlight} />
|
||||
}
|
||||
cancelLabel={t("common:cancel")}
|
||||
confirmLabel={t(ARCHIVE_LABEL_KEY)}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { CleanupSummary } from "./task-cleanup-summary";
|
||||
|
||||
type TaskCleanupConsequencesProps = {
|
||||
summary: CleanupSummary;
|
||||
compact?: boolean;
|
||||
};
|
||||
|
||||
export function TaskCleanupConsequences({
|
||||
summary,
|
||||
compact = false,
|
||||
}: TaskCleanupConsequencesProps) {
|
||||
const effectsClass = cn(
|
||||
"min-w-0 [overflow-wrap:anywhere]",
|
||||
compact ? "space-y-0.5" : "list-disc space-y-1.5 pl-5",
|
||||
);
|
||||
const notesClass = cn(
|
||||
"min-w-0 text-muted-foreground [overflow-wrap:anywhere]",
|
||||
compact ? "space-y-0.5" : "space-y-1",
|
||||
);
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<span data-testid="task-cleanup-consequences" className="block min-w-0 space-y-1">
|
||||
<span data-testid="task-cleanup-effects" role="list" className={effectsClass}>
|
||||
{summary.effects.map((effect, index) => (
|
||||
<span key={index} role="listitem" className="block">
|
||||
{effect}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
{summary.notes.length > 0 && (
|
||||
<span data-testid="task-cleanup-notes" className={cn("block", notesClass)}>
|
||||
{summary.notes.map((note, index) => (
|
||||
<span key={index} className="block" data-testid="task-cleanup-note">
|
||||
{note}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-testid="task-cleanup-consequences" className="min-w-0 space-y-2">
|
||||
<ul data-testid="task-cleanup-effects" className={effectsClass}>
|
||||
{summary.effects.map((effect, index) => (
|
||||
<li key={index} data-testid="task-cleanup-effect">
|
||||
{effect}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{summary.notes.length > 0 && (
|
||||
<div data-testid="task-cleanup-notes" className={notesClass}>
|
||||
{summary.notes.map((note, index) => (
|
||||
<p key={index} data-testid="task-cleanup-note">
|
||||
{note}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { afterEach, describe, it, expect } from "vitest";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { i18n } from "@/lib/i18n";
|
||||
import { getCleanupSummary, getBulkCleanupSummary } from "./task-cleanup-summary";
|
||||
|
||||
const AGENT_STOP_LINE = "Any running agent sessions will be stopped.";
|
||||
const AGENT_STOP_EFFECT = "Any running agent sessions will be stopped.";
|
||||
|
||||
afterEach(async () => {
|
||||
await i18n.changeLanguage("en");
|
||||
@@ -10,144 +10,174 @@ afterEach(async () => {
|
||||
|
||||
describe("getCleanupSummary (single task)", () => {
|
||||
it("reassures the user that local executor leaves the repo untouched", () => {
|
||||
const { lines } = getCleanupSummary("local");
|
||||
expect(lines[0]).toMatch(/directly in your repo/i);
|
||||
expect(lines[0]).toMatch(/not touched/i);
|
||||
const summary = getCleanupSummary("local");
|
||||
expect(summary.effects).toEqual([AGENT_STOP_EFFECT]);
|
||||
expect(summary.notes).toHaveLength(1);
|
||||
expect(summary.notes[0]).toMatch(/directly in your repo/i);
|
||||
expect(summary.notes[0]).toMatch(/not touched/i);
|
||||
});
|
||||
|
||||
it("describes worktree deletion + branch removal", () => {
|
||||
const { lines } = getCleanupSummary("worktree");
|
||||
expect(lines[0]).toMatch(/worktree/i);
|
||||
expect(lines[0]).toMatch(/branch/i);
|
||||
expect(lines[0]).toMatch(/not affected/i);
|
||||
const { effects, notes } = getCleanupSummary("worktree");
|
||||
expect(effects[0]).toMatch(/worktree/i);
|
||||
expect(effects[0]).toMatch(/branch/i);
|
||||
expect(effects[0]).toMatch(/deleted/i);
|
||||
expect(notes.join(" ")).toMatch(/not affected/i);
|
||||
});
|
||||
|
||||
it("describes local_docker container removal", () => {
|
||||
const { lines } = getCleanupSummary("local_docker");
|
||||
expect(lines[0]).toMatch(/Docker container/i);
|
||||
expect(lines[0]).toMatch(/removed/i);
|
||||
const { effects, notes } = getCleanupSummary("local_docker");
|
||||
expect(effects[0]).toMatch(/Docker container/i);
|
||||
expect(effects[0]).toMatch(/removed/i);
|
||||
expect(notes.join(" ")).toMatch(/host repo/i);
|
||||
});
|
||||
|
||||
it("describes remote_docker container removal", () => {
|
||||
const { lines } = getCleanupSummary("remote_docker");
|
||||
expect(lines[0]).toMatch(/remote Docker container/i);
|
||||
const { effects, notes } = getCleanupSummary("remote_docker");
|
||||
expect(effects[0]).toMatch(/remote Docker container/i);
|
||||
expect(effects[0]).toMatch(/removed/i);
|
||||
expect(notes).toEqual([]);
|
||||
});
|
||||
|
||||
it("warns that sprites sandbox destruction loses uncommitted work", () => {
|
||||
const { lines } = getCleanupSummary("sprites");
|
||||
expect(lines[0]).toMatch(/Sprites/i);
|
||||
expect(lines[0]).toMatch(/uncommitted work/i);
|
||||
const { effects } = getCleanupSummary("sprites");
|
||||
expect(effects[0]).toMatch(/Sprites/i);
|
||||
expect(effects[0]).toMatch(/uncommitted work/i);
|
||||
});
|
||||
|
||||
it("describes ssh remote directory removal as best-effort", () => {
|
||||
const { lines } = getCleanupSummary("ssh");
|
||||
expect(lines[0]).toMatch(/remote/i);
|
||||
expect(lines[0]).toMatch(/best-effort/i);
|
||||
const { effects, notes } = getCleanupSummary("ssh");
|
||||
expect(effects[0]).toMatch(/remote/i);
|
||||
expect(effects[0]).toMatch(/removed/i);
|
||||
expect(notes.join(" ")).toMatch(/best-effort/i);
|
||||
});
|
||||
|
||||
it("always appends the agent-session-stop line", () => {
|
||||
expect(getCleanupSummary("local").lines).toContain(AGENT_STOP_LINE);
|
||||
expect(getCleanupSummary("worktree").lines).toContain(AGENT_STOP_LINE);
|
||||
it("always appends the agent-session-stop effect", () => {
|
||||
expect(getCleanupSummary("local").effects).toContain(AGENT_STOP_EFFECT);
|
||||
expect(getCleanupSummary("worktree").effects).toContain(AGENT_STOP_EFFECT);
|
||||
});
|
||||
|
||||
it("falls back to a generic line when executor type is unknown or missing", () => {
|
||||
expect(getCleanupSummary(undefined).lines).toEqual([AGENT_STOP_LINE]);
|
||||
expect(getCleanupSummary(null).lines).toEqual([AGENT_STOP_LINE]);
|
||||
expect(getCleanupSummary("totally-made-up").lines).toEqual([AGENT_STOP_LINE]);
|
||||
it("falls back to a generic effect when executor type is unknown or missing", () => {
|
||||
expect(getCleanupSummary(undefined)).toEqual({
|
||||
effects: [AGENT_STOP_EFFECT],
|
||||
notes: [],
|
||||
});
|
||||
expect(getCleanupSummary(null)).toEqual({ effects: [AGENT_STOP_EFFECT], notes: [] });
|
||||
expect(getCleanupSummary("totally-made-up")).toEqual({
|
||||
effects: [AGENT_STOP_EFFECT],
|
||||
notes: [],
|
||||
});
|
||||
expect(getCleanupSummary("constructor")).toEqual({
|
||||
effects: [AGENT_STOP_EFFECT],
|
||||
notes: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("is case-insensitive for executor type", () => {
|
||||
expect(getCleanupSummary("WORKTREE").lines[0]).toMatch(/worktree/i);
|
||||
expect(getCleanupSummary("Local").lines[0]).toMatch(/directly in your repo/i);
|
||||
expect(getCleanupSummary("WORKTREE").effects[0]).toMatch(/worktree/i);
|
||||
expect(getCleanupSummary("Local").notes.join(" ")).toMatch(/directly in your repo/i);
|
||||
});
|
||||
|
||||
it("returns structured effects and notes instead of a flat line list", () => {
|
||||
const summary = getCleanupSummary("worktree");
|
||||
|
||||
expect(summary).not.toHaveProperty("lines");
|
||||
expect(Array.isArray(summary.effects)).toBe(true);
|
||||
expect(Array.isArray(summary.notes)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBulkCleanupSummary", () => {
|
||||
it("groups by executor type and counts each group", () => {
|
||||
const { lines } = getBulkCleanupSummary([
|
||||
const { effects, notes } = getBulkCleanupSummary([
|
||||
"worktree",
|
||||
"worktree",
|
||||
"local_docker",
|
||||
"local",
|
||||
"sprites",
|
||||
]);
|
||||
expect(lines.some((l) => /2 worktrees/.test(l) && /branches/.test(l))).toBe(true);
|
||||
expect(lines.some((l) => /1 Docker container/.test(l))).toBe(true);
|
||||
expect(lines.some((l) => /1 local task/.test(l) && /won't be touched/.test(l))).toBe(true);
|
||||
expect(lines.some((l) => /1 Sprites sandbox/.test(l))).toBe(true);
|
||||
expect(effects.some((line) => /2 worktrees/.test(line) && /branches/.test(line))).toBe(true);
|
||||
expect(effects.some((line) => /1 Docker container/.test(line))).toBe(true);
|
||||
expect(notes.some((line) => /1 local task/.test(line) && /won't be touched/.test(line))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(effects.some((line) => /1 Sprites sandbox/.test(line))).toBe(true);
|
||||
});
|
||||
|
||||
it("uses singular wording for groups of size one", () => {
|
||||
const { lines } = getBulkCleanupSummary(["worktree", "local_docker"]);
|
||||
expect(lines.some((l) => /1 worktree\b/.test(l))).toBe(true);
|
||||
expect(lines.some((l) => /1 Docker container\b/.test(l))).toBe(true);
|
||||
const { effects } = getBulkCleanupSummary(["worktree", "local_docker"]);
|
||||
expect(effects.some((line) => /1 worktree\b/.test(line))).toBe(true);
|
||||
expect(effects.some((line) => /1 Docker container\b/.test(line))).toBe(true);
|
||||
});
|
||||
|
||||
it("uses plural wording for groups of size > 1", () => {
|
||||
const { lines } = getBulkCleanupSummary(["worktree", "worktree", "worktree"]);
|
||||
expect(lines.some((l) => /3 worktrees/.test(l))).toBe(true);
|
||||
const { effects } = getBulkCleanupSummary(["worktree", "worktree", "worktree"]);
|
||||
expect(effects.some((line) => /3 worktrees/.test(line))).toBe(true);
|
||||
});
|
||||
|
||||
it("groups remote_docker tasks with their own line", () => {
|
||||
const { lines } = getBulkCleanupSummary(["remote_docker", "remote_docker"]);
|
||||
expect(lines.some((l) => /2 remote Docker containers/.test(l))).toBe(true);
|
||||
const { effects } = getBulkCleanupSummary(["remote_docker", "remote_docker"]);
|
||||
expect(effects.some((line) => /2 remote Docker containers/.test(line))).toBe(true);
|
||||
});
|
||||
|
||||
it("groups ssh tasks with best-effort wording", () => {
|
||||
const { lines } = getBulkCleanupSummary(["ssh", "ssh"]);
|
||||
expect(lines.some((l) => /2 remote task directories/.test(l) && /best-effort/.test(l))).toBe(
|
||||
true,
|
||||
);
|
||||
const { effects, notes } = getBulkCleanupSummary(["ssh", "ssh"]);
|
||||
expect(effects.some((line) => /2 remote task directories/.test(line))).toBe(true);
|
||||
expect(notes.join(" ")).toMatch(/best-effort/i);
|
||||
});
|
||||
|
||||
it("emits worktree/container/sandbox lines before the local-reassurance line", () => {
|
||||
const { lines } = getBulkCleanupSummary(["local", "worktree"]);
|
||||
const worktreeIdx = lines.findIndex((l) => /worktree/i.test(l));
|
||||
const localIdx = lines.findIndex((l) => /local task/i.test(l));
|
||||
expect(worktreeIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(localIdx).toBeGreaterThan(worktreeIdx);
|
||||
it("keeps resource effects separate from local-reassurance notes", () => {
|
||||
const { effects, notes } = getBulkCleanupSummary(["local", "worktree"]);
|
||||
expect(effects.findIndex((line) => /worktree/i.test(line))).toBeGreaterThanOrEqual(0);
|
||||
expect(notes.findIndex((line) => /local task/i.test(line))).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it("always appends the agent-session-stop line", () => {
|
||||
const { lines } = getBulkCleanupSummary(["worktree"]);
|
||||
expect(lines[lines.length - 1]).toBe(AGENT_STOP_LINE);
|
||||
it("always appends the agent-session-stop effect", () => {
|
||||
const { effects } = getBulkCleanupSummary(["worktree"]);
|
||||
expect(effects[effects.length - 1]).toBe(AGENT_STOP_EFFECT);
|
||||
});
|
||||
|
||||
it("ignores unknown / mock executor types in the grouped output", () => {
|
||||
const { lines } = getBulkCleanupSummary(["worktree", undefined, "mock_remote", "what"]);
|
||||
expect(lines.some((l) => /worktree/i.test(l))).toBe(true);
|
||||
expect(lines.some((l) => /mock/i.test(l))).toBe(false);
|
||||
expect(lines.some((l) => /what/i.test(l))).toBe(false);
|
||||
const { effects, notes } = getBulkCleanupSummary([
|
||||
"worktree",
|
||||
undefined,
|
||||
"mock_remote",
|
||||
"what",
|
||||
]);
|
||||
expect(effects.some((line) => /worktree/i.test(line))).toBe(true);
|
||||
expect(effects.some((line) => /mock/i.test(line))).toBe(false);
|
||||
expect(effects.some((line) => /what/i.test(line))).toBe(false);
|
||||
expect(notes).toEqual(getBulkCleanupSummary(["worktree"]).notes);
|
||||
});
|
||||
|
||||
it("returns only the generic line for an empty input", () => {
|
||||
expect(getBulkCleanupSummary([]).lines).toEqual([AGENT_STOP_LINE]);
|
||||
it("returns only the generic effect for an empty input", () => {
|
||||
expect(getBulkCleanupSummary([])).toEqual({
|
||||
effects: [AGENT_STOP_EFFECT],
|
||||
notes: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("localization", () => {
|
||||
it("resolves at call time, so a locale switch changes the copy", async () => {
|
||||
await i18n.changeLanguage("pseudo");
|
||||
const { lines } = getCleanupSummary("worktree");
|
||||
// Every line, not just the first — the generic trailer used to be a separate
|
||||
// module constant and would have stayed English while the rest translated.
|
||||
for (const line of lines) expect(line).toMatch(/[^\p{ASCII}]/u);
|
||||
const { effects, notes } = getCleanupSummary("worktree");
|
||||
// Every item, not just the first. The generic trailer must follow locale switches too.
|
||||
for (const line of [...effects, ...notes]) expect(line).toMatch(/[^\p{ASCII}]/u);
|
||||
});
|
||||
|
||||
it("routes the bulk counts through catalog plurals rather than English suffixes", async () => {
|
||||
await i18n.changeLanguage("pseudo");
|
||||
const { lines } = getBulkCleanupSummary(["worktree", "worktree"]);
|
||||
// The count is interpolated, so it survives pseudo; the words around it must
|
||||
// not. A locally-built `n === 1 ? "worktree" : "worktrees"` would leave the
|
||||
// noun in plain English here.
|
||||
const worktreeLine = lines.find((line) => line.includes("2"));
|
||||
const { effects } = getBulkCleanupSummary(["worktree", "worktree"]);
|
||||
// The count is interpolated, so it survives pseudo. The surrounding words must not.
|
||||
const worktreeLine = effects.find((line) => line.includes("2"));
|
||||
expect(worktreeLine).toBeDefined();
|
||||
expect(worktreeLine).not.toMatch(/worktrees/);
|
||||
expect(worktreeLine).toMatch(/[^\p{ASCII}]/u);
|
||||
});
|
||||
|
||||
it("picks the singular catalog form for a group of one", () => {
|
||||
const { lines } = getBulkCleanupSummary(["sprites"]);
|
||||
expect(lines[0]).toBe("1 Sprites sandbox will be destroyed (uncommitted work lost).");
|
||||
const { effects } = getBulkCleanupSummary(["sprites"]);
|
||||
expect(effects[0]).toBe("1 Sprites sandbox will be destroyed (uncommitted work lost).");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,70 +2,87 @@
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
export type CleanupSummary = {
|
||||
lines: string[];
|
||||
effects: string[];
|
||||
notes: string[];
|
||||
};
|
||||
|
||||
// mock_remote is test-only — intentionally absent so it falls through to GENERIC_LINE.
|
||||
// mock_remote is test-only and intentionally falls through to the generic effect.
|
||||
type KnownExecutor = "local" | "worktree" | "local_docker" | "remote_docker" | "sprites" | "ssh";
|
||||
|
||||
/**
|
||||
* Catalog keys, not copy.
|
||||
*
|
||||
* Resolved in `getCleanupSummary` / `getBulkCleanupSummary`, which the delete
|
||||
* and archive dialogs call from their render bodies — so a locale switch
|
||||
* re-renders them and the lines follow. Holding `t()` results here instead would
|
||||
* freeze this copy at the boot locale.
|
||||
*/
|
||||
const SINGLE_KEYS: Record<KnownExecutor, string> = {
|
||||
local: "task:cleanupSingleLocal",
|
||||
worktree: "task:cleanupSingleWorktree",
|
||||
local_docker: "task:cleanupSingleLocalDocker",
|
||||
remote_docker: "task:cleanupSingleRemoteDocker",
|
||||
sprites: "task:cleanupSingleSprites",
|
||||
ssh: "task:cleanupSingleSsh",
|
||||
type CleanupCopy = {
|
||||
effectKey?: string;
|
||||
noteKey?: string;
|
||||
};
|
||||
|
||||
const GENERIC_KEY = "task:cleanupAgentSessionsStopped";
|
||||
/**
|
||||
* Catalog keys, not copy. The values resolve inside each public function so a
|
||||
* locale switch updates both the effect list and the supporting notes.
|
||||
*/
|
||||
const SINGLE_COPIES: Record<KnownExecutor, CleanupCopy> = {
|
||||
local: { noteKey: "task:cleanupSingleLocal" },
|
||||
worktree: {
|
||||
effectKey: "task:cleanupSingleWorktree",
|
||||
noteKey: "task:cleanupSingleWorktreeNote",
|
||||
},
|
||||
local_docker: {
|
||||
effectKey: "task:cleanupSingleLocalDocker",
|
||||
noteKey: "task:cleanupSingleLocalDockerNote",
|
||||
},
|
||||
remote_docker: { effectKey: "task:cleanupSingleRemoteDocker" },
|
||||
sprites: { effectKey: "task:cleanupSingleSprites" },
|
||||
ssh: {
|
||||
effectKey: "task:cleanupSingleSsh",
|
||||
noteKey: "task:cleanupSingleSshNote",
|
||||
},
|
||||
};
|
||||
|
||||
const GENERIC_EFFECT_KEY = "task:cleanupAgentSessionsStopped";
|
||||
|
||||
function normalize(executorType: string | null | undefined): KnownExecutor | null {
|
||||
if (!executorType) return null;
|
||||
const key = executorType.toLowerCase();
|
||||
if (key in SINGLE_KEYS) return key as KnownExecutor;
|
||||
if (Object.hasOwn(SINGLE_COPIES, key)) return key as KnownExecutor;
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveCopy(copy: CleanupCopy, options?: { count: number }): CleanupSummary {
|
||||
const effects = copy.effectKey ? [t(copy.effectKey, options)] : [];
|
||||
effects.push(t(GENERIC_EFFECT_KEY));
|
||||
const notes = copy.noteKey ? [t(copy.noteKey, options)] : [];
|
||||
return { effects, notes };
|
||||
}
|
||||
|
||||
/** Single-task variant. */
|
||||
export function getCleanupSummary(executorType: string | null | undefined): CleanupSummary {
|
||||
const generic = t(GENERIC_KEY);
|
||||
const known = normalize(executorType);
|
||||
if (!known) return { lines: [generic] };
|
||||
return { lines: [t(SINGLE_KEYS[known]), generic] };
|
||||
return known
|
||||
? resolveCopy(SINGLE_COPIES[known])
|
||||
: { effects: [t(GENERIC_EFFECT_KEY)], notes: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Catalog keys for the grouped variant.
|
||||
*
|
||||
* Each resolves with `{ count }` against `_one`/`_other` entries, so the
|
||||
* singular/plural split is the catalog's decision. The previous version built it
|
||||
* here with a local `pl(n, "worktree", "worktrees")` helper — English-only by
|
||||
* construction, and wrong in every locale whose plural rules are not English's.
|
||||
*/
|
||||
const PLURAL_KEYS: Record<KnownExecutor, string> = {
|
||||
local: "task:cleanupBulkLocal",
|
||||
worktree: "task:cleanupBulkWorktree",
|
||||
local_docker: "task:cleanupBulkLocalDocker",
|
||||
remote_docker: "task:cleanupBulkRemoteDocker",
|
||||
sprites: "task:cleanupBulkSprites",
|
||||
ssh: "task:cleanupBulkSsh",
|
||||
/** Catalog keys for the grouped variant. */
|
||||
const BULK_COPIES: Record<KnownExecutor, CleanupCopy> = {
|
||||
local: { noteKey: "task:cleanupBulkLocal" },
|
||||
worktree: {
|
||||
effectKey: "task:cleanupBulkWorktree",
|
||||
noteKey: "task:cleanupBulkWorktreeNote",
|
||||
},
|
||||
local_docker: {
|
||||
effectKey: "task:cleanupBulkLocalDocker",
|
||||
noteKey: "task:cleanupBulkLocalDockerNote",
|
||||
},
|
||||
remote_docker: { effectKey: "task:cleanupBulkRemoteDocker" },
|
||||
sprites: { effectKey: "task:cleanupBulkSprites" },
|
||||
ssh: {
|
||||
effectKey: "task:cleanupBulkSsh",
|
||||
noteKey: "task:cleanupBulkSshNote",
|
||||
},
|
||||
};
|
||||
|
||||
/** Bulk variant — groups N tasks by executor type and emits one line per group. */
|
||||
/** Bulk variant. Groups known tasks by executor type and preserves display order. */
|
||||
export function getBulkCleanupSummary(
|
||||
executorTypes: Array<string | null | undefined>,
|
||||
): CleanupSummary {
|
||||
const generic = t(GENERIC_KEY);
|
||||
if (executorTypes.length === 0) return { lines: [generic] };
|
||||
|
||||
const counts = new Map<KnownExecutor, number>();
|
||||
for (const executorType of executorTypes) {
|
||||
const known = normalize(executorType);
|
||||
@@ -81,13 +98,15 @@ export function getBulkCleanupSummary(
|
||||
"ssh",
|
||||
"local",
|
||||
];
|
||||
|
||||
const lines: string[] = [];
|
||||
const effects: string[] = [];
|
||||
const notes: string[] = [];
|
||||
for (const key of order) {
|
||||
const count = counts.get(key);
|
||||
if (!count) continue;
|
||||
lines.push(t(PLURAL_KEYS[key], { count }));
|
||||
const copy = BULK_COPIES[key];
|
||||
if (copy.effectKey) effects.push(t(copy.effectKey, { count }));
|
||||
if (copy.noteKey) notes.push(t(copy.noteKey, { count }));
|
||||
}
|
||||
lines.push(generic);
|
||||
return { lines };
|
||||
effects.push(t(GENERIC_EFFECT_KEY));
|
||||
return { effects, notes };
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import type { MouseEvent } from "react";
|
||||
|
||||
export const TASK_CONFIRM_CLASS = "font-sans !text-sm";
|
||||
export const TASK_CONFIRM_CLASS =
|
||||
"font-sans !text-sm max-h-[calc(100dvh-2rem)] grid-rows-[auto_minmax(0,1fr)_auto] overflow-hidden";
|
||||
export const TASK_CONFIRM_HEADER_CLASS = "place-items-start text-left";
|
||||
export const TASK_CONFIRM_BODY_CLASS =
|
||||
"min-h-0 min-w-0 space-y-3 overflow-y-auto overscroll-contain text-left";
|
||||
export const TASK_CONFIRM_FOOTER_CLASS = "w-full sm:w-auto";
|
||||
export const TASK_CONFIRM_ACTION_CLASS =
|
||||
"cursor-pointer !text-sm min-h-11 w-full sm:min-h-0 sm:w-auto";
|
||||
export const stopDialogPropagation = (event: MouseEvent<HTMLDivElement>) => event.stopPropagation();
|
||||
|
||||
@@ -49,6 +49,33 @@ beforeEach(() => {
|
||||
afterEach(cleanup);
|
||||
|
||||
describe("TaskDeleteConfirmDialog", () => {
|
||||
it("contains long confirmation content in a scrolling body with touch-safe actions", () => {
|
||||
mockGetSubtaskCount.mockResolvedValue({ count: 0 });
|
||||
renderDialog(
|
||||
<TaskDeleteConfirmDialog
|
||||
open
|
||||
onOpenChange={() => {}}
|
||||
taskTitle="A task with a title that needs to wrap inside a phone confirmation surface"
|
||||
taskId="task-1"
|
||||
executorType="sprites"
|
||||
isInFlight
|
||||
confirmTestId="confirm"
|
||||
onConfirm={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const dialog = screen.getByRole("alertdialog");
|
||||
expect(dialog.className).toContain("max-h-[calc(100dvh-2rem)]");
|
||||
expect(dialog.className).toContain("grid-rows-[auto_minmax(0,1fr)_auto]");
|
||||
expect(dialog.className).toContain("overflow-hidden");
|
||||
expect(screen.getByTestId("task-confirmation-body").className).toContain("min-h-0");
|
||||
expect(screen.getByTestId("task-confirmation-body").className).toContain("space-y-3");
|
||||
expect(screen.getByTestId("task-confirmation-body").className).toContain("overflow-y-auto");
|
||||
expect(screen.getByTestId("confirm").className).toContain("min-h-11");
|
||||
expect(screen.getByTestId("confirm").className).toContain("w-full");
|
||||
expect(screen.getByTestId("confirm").getAttribute("data-variant")).toBe("destructive");
|
||||
});
|
||||
|
||||
it("hides the cascade checkbox when the task has no subtasks", async () => {
|
||||
mockGetSubtaskCount.mockResolvedValue({ count: 0 });
|
||||
const onConfirm = vi.fn();
|
||||
@@ -127,6 +154,28 @@ describe("TaskDeleteConfirmDialog", () => {
|
||||
});
|
||||
|
||||
describe("TaskDeleteConfirmDialog executor cleanup copy", () => {
|
||||
it("states the named delete outcome directly and separates cleanup effects", () => {
|
||||
mockGetSubtaskCount.mockResolvedValue({ count: 0 });
|
||||
renderDialog(
|
||||
<TaskDeleteConfirmDialog
|
||||
open
|
||||
onOpenChange={() => {}}
|
||||
taskTitle="My task"
|
||||
taskId="task-1"
|
||||
executorType="worktree"
|
||||
onConfirm={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("task-confirmation-outcome").textContent).toMatch(
|
||||
/Delete [“"]?My task[”"]?\. This action cannot be undone\./i,
|
||||
);
|
||||
expect(screen.getByTestId("task-cleanup-effects").tagName).toBe("UL");
|
||||
expect(screen.getByTestId("task-cleanup-effects").querySelectorAll("li")).toHaveLength(2);
|
||||
expect(screen.getByTestId("task-cleanup-notes").tagName).toBe("DIV");
|
||||
expect(screen.queryByText(/Are you sure/i)).toBeNull();
|
||||
});
|
||||
|
||||
it("local reassures repo is untouched", async () => {
|
||||
mockGetSubtaskCount.mockResolvedValue({ count: 0 });
|
||||
renderDialog(
|
||||
|
||||
@@ -16,9 +16,13 @@ import { Checkbox } from "@kandev/ui/checkbox";
|
||||
import { useSubtaskCount } from "@/hooks/use-subtask-count";
|
||||
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 {
|
||||
TASK_CONFIRM_ACTION_CLASS,
|
||||
TASK_CONFIRM_BODY_CLASS,
|
||||
TASK_CONFIRM_CLASS,
|
||||
TASK_CONFIRM_FOOTER_CLASS,
|
||||
TASK_CONFIRM_HEADER_CLASS,
|
||||
stopDialogPropagation,
|
||||
} from "./task-confirm-dialog-shared";
|
||||
@@ -83,43 +87,42 @@ export function TaskDeleteConfirmDialog({
|
||||
<AlertDialogContent size="lg" className={TASK_CONFIRM_CLASS} onClick={stopDialogPropagation}>
|
||||
<AlertDialogHeader className={TASK_CONFIRM_HEADER_CLASS}>
|
||||
<AlertDialogTitle className="text-base font-semibold">{title}</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild className="text-sm leading-6">
|
||||
<div>
|
||||
<p>{description}</p>
|
||||
{cleanup.lines.map((line, i) => (
|
||||
<p key={i} className="mt-2" data-testid="cleanup-line">
|
||||
{line}
|
||||
</p>
|
||||
))}
|
||||
</AlertDialogHeader>
|
||||
<div data-testid="task-confirmation-body" className={TASK_CONFIRM_BODY_CLASS}>
|
||||
<AlertDialogDescription asChild className="text-left text-sm leading-6">
|
||||
<div className="space-y-3">
|
||||
<p data-testid="task-confirmation-outcome">{description}</p>
|
||||
<TaskCleanupConsequences summary={cleanup} />
|
||||
</div>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
{(isInFlight || storeInFlight) && (
|
||||
<StillWorkingWarning count={isBulkOperation ? safeCount : undefined} />
|
||||
)}
|
||||
{subtaskCount > 0 && (
|
||||
<label className="flex items-start gap-2 text-sm cursor-pointer">
|
||||
<Checkbox
|
||||
checked={cascade}
|
||||
onCheckedChange={(v) => setCascade(v === true)}
|
||||
disabled={isDeleting}
|
||||
data-testid="delete-cascade-checkbox"
|
||||
/>
|
||||
<span>
|
||||
{t("task:alsoDeleteSubtasks", { count: subtaskCount })}
|
||||
<span className="block text-sm text-muted-foreground">
|
||||
{t("task:subtasksBecomeRootTasksUnlessYou")}
|
||||
{(isInFlight || storeInFlight) && (
|
||||
<StillWorkingWarning count={isBulkOperation ? safeCount : undefined} />
|
||||
)}
|
||||
{subtaskCount > 0 && (
|
||||
<label className="flex cursor-pointer items-start gap-2 text-sm">
|
||||
<Checkbox
|
||||
checked={cascade}
|
||||
onCheckedChange={(v) => setCascade(v === true)}
|
||||
disabled={isDeleting}
|
||||
data-testid="delete-cascade-checkbox"
|
||||
/>
|
||||
<span>
|
||||
{t("task:alsoDeleteSubtasks", { count: subtaskCount })}
|
||||
<span className="block text-sm text-muted-foreground">
|
||||
{t("task:subtasksBecomeRootTasksUnlessYou")}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel className="cursor-pointer !text-sm">
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
<AlertDialogFooter className={TASK_CONFIRM_FOOTER_CLASS}>
|
||||
<AlertDialogCancel className={TASK_CONFIRM_ACTION_CLASS}>
|
||||
{t("common:cancel")}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
variant="destructive"
|
||||
disabled={isDeleting}
|
||||
className="cursor-pointer bg-destructive !text-sm text-destructive-foreground hover:bg-destructive/90"
|
||||
className={TASK_CONFIRM_ACTION_CLASS}
|
||||
data-testid={confirmTestId}
|
||||
onClick={() => {
|
||||
if (isDeleting) return;
|
||||
|
||||
@@ -103,10 +103,10 @@
|
||||
"approvedVerb": "Approved",
|
||||
"approvers": "Approvers",
|
||||
"archive": "Archive",
|
||||
"archiveTaskConfirm": "Are you sure you want to archive \"{{taskTitle}}\"?",
|
||||
"archiveTaskConfirm": "Archive \"{{taskTitle}}\".",
|
||||
"archiveTaskTitle": "Archive task?",
|
||||
"archiveTasksConfirm_one": "Are you sure you want to archive {{count}} task?",
|
||||
"archiveTasksConfirm_other": "Are you sure you want to archive {{count}} tasks?",
|
||||
"archiveTasksConfirm_one": "Archive {{count}} task.",
|
||||
"archiveTasksConfirm_other": "Archive {{count}} tasks.",
|
||||
"archiveTasksTitle_one": "Archive {{count}} task?",
|
||||
"archiveTasksTitle_other": "Archive {{count}} tasks?",
|
||||
"archiveTasks_one": "Archive {{count}} task",
|
||||
@@ -329,10 +329,10 @@
|
||||
"deleteItemsTitle_other": "Delete {{count}} items?",
|
||||
"deleteLayoutConfirm": "Delete {{name}}?",
|
||||
"deleteSession": "Delete session?",
|
||||
"deleteTaskConfirm": "Are you sure you want to delete \"{{taskTitle}}\"? This action cannot be undone.",
|
||||
"deleteTaskConfirm": "Delete \"{{taskTitle}}\". This action cannot be undone.",
|
||||
"deleteTaskTitle": "Delete task",
|
||||
"deleteTasksConfirm_one": "Are you sure you want to delete {{count}} task? This action cannot be undone.",
|
||||
"deleteTasksConfirm_other": "Are you sure you want to delete {{count}} tasks? This action cannot be undone.",
|
||||
"deleteTasksConfirm_one": "Delete {{count}} task. This action cannot be undone.",
|
||||
"deleteTasksConfirm_other": "Delete {{count}} tasks. This action cannot be undone.",
|
||||
"deleteTasksCount_one": "Delete {{count}} task",
|
||||
"deleteTasksCount_other": "Delete {{count}} tasks",
|
||||
"deleteTasksTitle_one": "Delete {{count}} task",
|
||||
@@ -1841,24 +1841,30 @@
|
||||
"branchTooltipGeneric": "Branch the agent will run against.",
|
||||
"branchTooltipWillSwitchTo": "The backend will run `git checkout` to switch your repository to this branch before the agent starts.",
|
||||
"cleanupAgentSessionsStopped": "Any running agent sessions will be stopped.",
|
||||
"cleanupBulkLocal_one": "{{count}} local task - your repo folders won't be touched.",
|
||||
"cleanupBulkLocal_other": "{{count}} local tasks - your repo folders won't be touched.",
|
||||
"cleanupBulkLocal_one": "{{count}} local task ran directly in your repo. Your repo folders won't be touched.",
|
||||
"cleanupBulkLocal_other": "{{count}} local tasks ran directly in your repo. Your repo folders won't be touched.",
|
||||
"cleanupBulkLocalDocker_one": "{{count}} Docker container will be removed.",
|
||||
"cleanupBulkLocalDocker_other": "{{count}} Docker containers will be removed.",
|
||||
"cleanupBulkRemoteDocker_one": "{{count}} remote Docker container will be removed.",
|
||||
"cleanupBulkRemoteDocker_other": "{{count}} remote Docker containers will be removed.",
|
||||
"cleanupBulkSprites_one": "{{count}} Sprites sandbox will be destroyed (uncommitted work lost).",
|
||||
"cleanupBulkSprites_other": "{{count}} Sprites sandboxes will be destroyed (uncommitted work lost).",
|
||||
"cleanupBulkSsh_one": "{{count}} remote task directory will be removed (best-effort).",
|
||||
"cleanupBulkSsh_other": "{{count}} remote task directories will be removed (best-effort).",
|
||||
"cleanupBulkSsh_one": "{{count}} remote task directory will be removed.",
|
||||
"cleanupBulkSsh_other": "{{count}} remote task directories will be removed.",
|
||||
"cleanupBulkSshNote": "Best-effort removal may not complete. Your local repo is not touched.",
|
||||
"cleanupBulkWorktree_one": "{{count}} worktree and its branch will be deleted.",
|
||||
"cleanupBulkWorktree_other": "{{count}} worktrees and their branches will be deleted.",
|
||||
"cleanupSingleLocal": "The agent ran directly in your repo - your files, branch, and folder are not touched.",
|
||||
"cleanupSingleLocalDocker": "The Docker container running this task will be stopped and removed. Your host repo is not touched.",
|
||||
"cleanupBulkWorktreeNote": "Your main repo and other branches are not affected.",
|
||||
"cleanupBulkLocalDockerNote": "Your host repo is not touched.",
|
||||
"cleanupSingleLocal": "The agent ran directly in your repo. Your files, branch, and folder are not touched.",
|
||||
"cleanupSingleLocalDocker": "The Docker container running this task will be stopped and removed.",
|
||||
"cleanupSingleLocalDockerNote": "Your host repo is not touched.",
|
||||
"cleanupSingleRemoteDocker": "The remote Docker container running this task will be stopped and removed.",
|
||||
"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 (best-effort). Your local repo is not touched.",
|
||||
"cleanupSingleWorktree": "The task's git worktree and its branch will be deleted. Your main repo and other branches are not affected.",
|
||||
"cleanupSingleSsh": "The task directory on the remote host will be removed.",
|
||||
"cleanupSingleSshNote": "Best-effort removal may not complete. Your local repo is 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.",
|
||||
"enhancedPromptCouldNotBeCopied": "Enhanced prompt could not be copied.",
|
||||
"enhancedPromptCouldNotBeInserted": "Enhanced prompt was generated but could not be inserted.",
|
||||
|
||||
@@ -103,10 +103,10 @@
|
||||
"approvedVerb": "Àƥƥŕōvēď",
|
||||
"approvers": "Àƥƥŕōvēŕś",
|
||||
"archive": "Àŕćĥĩvē",
|
||||
"archiveTaskConfirm": "Àŕē ŷōũ śũŕē ŷōũ ŵàńţ ţō àŕćĥĩvē \"{{taskTitle}}\"?",
|
||||
"archiveTaskConfirm": "Àŕćĥĩvē \"{{taskTitle}}\".",
|
||||
"archiveTaskTitle": "Àŕćĥĩvē ţàśķ?",
|
||||
"archiveTasksConfirm_one": "Àŕē ŷōũ śũŕē ŷōũ ŵàńţ ţō àŕćĥĩvē {{count}} ţàśķ?",
|
||||
"archiveTasksConfirm_other": "Àŕē ŷōũ śũŕē ŷōũ ŵàńţ ţō àŕćĥĩvē {{count}} ţàśķś?",
|
||||
"archiveTasksConfirm_one": "Àŕćĥĩvē {{count}} ţàśķ.",
|
||||
"archiveTasksConfirm_other": "Àŕćĥĩvē {{count}} ţàśķś.",
|
||||
"archiveTasksTitle_one": "Àŕćĥĩvē {{count}} ţàśķ?",
|
||||
"archiveTasksTitle_other": "Àŕćĥĩvē {{count}} ţàśķś?",
|
||||
"archiveTasks_one": "Àŕćĥĩvē {{count}} ţàśķ",
|
||||
@@ -329,10 +329,10 @@
|
||||
"deleteItemsTitle_other": "Ďēĺēţē {{count}} ĩţēḿś?",
|
||||
"deleteLayoutConfirm": "Ďēĺēţē {{name}}?",
|
||||
"deleteSession": "Ďēĺēţē śēśśĩōń?",
|
||||
"deleteTaskConfirm": "Àŕē ŷōũ śũŕē ŷōũ ŵàńţ ţō ďēĺēţē \"{{taskTitle}}\"? Ţĥĩś àćţĩōń ćàńńōţ ƀē ũńďōńē.",
|
||||
"deleteTaskConfirm": "Ďēĺēţē \"{{taskTitle}}\". Ţĥĩś àćţĩōń ćàńńōţ ƀē ũńďōńē.",
|
||||
"deleteTaskTitle": "Ďēĺēţē ţàśķ",
|
||||
"deleteTasksConfirm_one": "Àŕē ŷōũ śũŕē ŷōũ ŵàńţ ţō ďēĺēţē {{count}} ţàśķ? Ţĥĩś àćţĩōń ćàńńōţ ƀē ũńďōńē.",
|
||||
"deleteTasksConfirm_other": "Àŕē ŷōũ śũŕē ŷōũ ŵàńţ ţō ďēĺēţē {{count}} ţàśķś? Ţĥĩś àćţĩōń ćàńńōţ ƀē ũńďōńē.",
|
||||
"deleteTasksConfirm_one": "Ďēĺēţē {{count}} ţàśķ. Ţĥĩś àćţĩōń ćàńńōţ ƀē ũńďōńē.",
|
||||
"deleteTasksConfirm_other": "Ďēĺēţē {{count}} ţàśķś. Ţĥĩś àćţĩōń ćàńńōţ ƀē ũńďōńē.",
|
||||
"deleteTasksCount_one": "Ďēĺēţē {{count}} ţàśķ",
|
||||
"deleteTasksCount_other": "Ďēĺēţē {{count}} ţàśķś",
|
||||
"deleteTasksTitle_one": "Ďēĺēţē {{count}} ţàśķ",
|
||||
@@ -1841,24 +1841,30 @@
|
||||
"branchTooltipGeneric": "Ɓŕàńćĥ ţĥē àĝēńţ ŵĩĺĺ ŕũń àĝàĩńśţ.",
|
||||
"branchTooltipWillSwitchTo": "Ţĥē ƀàćķēńď ŵĩĺĺ ŕũń `ĝĩţ ćĥēćķōũţ` ţō śŵĩţćĥ ŷōũŕ ŕēƥōśĩţōŕŷ ţō ţĥĩś ƀŕàńćĥ ƀēƒōŕē ţĥē àĝēńţ śţàŕţś.",
|
||||
"cleanupAgentSessionsStopped": "Àńŷ ŕũńńĩńĝ àĝēńţ śēśśĩōńś ŵĩĺĺ ƀē śţōƥƥēď.",
|
||||
"cleanupBulkLocal_one": "{{count}} ĺōćàĺ ţàśķ - ŷōũŕ ŕēƥō ƒōĺďēŕś ŵōń'ţ ƀē ţōũćĥēď.",
|
||||
"cleanupBulkLocal_other": "{{count}} ĺōćàĺ ţàśķś - ŷōũŕ ŕēƥō ƒōĺďēŕś ŵōń'ţ ƀē ţōũćĥēď.",
|
||||
"cleanupBulkLocal_one": "{{count}} ĺōćàĺ ţàśķ ŕàń ďĩŕēćţĺŷ ĩń ŷōũŕ ŕēƥō. Ŷōũŕ ŕēƥō ƒōĺďēŕś ŵōń'ţ ƀē ţōũćĥēď.",
|
||||
"cleanupBulkLocal_other": "{{count}} ĺōćàĺ ţàśķś ŕàń ďĩŕēćţĺŷ ĩń ŷōũŕ ŕēƥō. Ŷōũŕ ŕēƥō ƒōĺďēŕś ŵōń'ţ ƀē ţōũćĥēď.",
|
||||
"cleanupBulkLocalDocker_one": "{{count}} Ďōćķēŕ ćōńţàĩńēŕ ŵĩĺĺ ƀē ŕēḿōvēď.",
|
||||
"cleanupBulkLocalDocker_other": "{{count}} Ďōćķēŕ ćōńţàĩńēŕś ŵĩĺĺ ƀē ŕēḿōvēď.",
|
||||
"cleanupBulkRemoteDocker_one": "{{count}} ŕēḿōţē Ďōćķēŕ ćōńţàĩńēŕ ŵĩĺĺ ƀē ŕēḿōvēď.",
|
||||
"cleanupBulkRemoteDocker_other": "{{count}} ŕēḿōţē Ďōćķēŕ ćōńţàĩńēŕś ŵĩĺĺ ƀē ŕēḿōvēď.",
|
||||
"cleanupBulkSprites_one": "{{count}} Śƥŕĩţēś śàńďƀōx ŵĩĺĺ ƀē ďēśţŕōŷēď (ũńćōḿḿĩţţēď ŵōŕķ ĺōśţ).",
|
||||
"cleanupBulkSprites_other": "{{count}} Śƥŕĩţēś śàńďƀōxēś ŵĩĺĺ ƀē ďēśţŕōŷēď (ũńćōḿḿĩţţēď ŵōŕķ ĺōśţ).",
|
||||
"cleanupBulkSsh_one": "{{count}} ŕēḿōţē ţàśķ ďĩŕēćţōŕŷ ŵĩĺĺ ƀē ŕēḿōvēď (ƀēśţ-ēƒƒōŕţ).",
|
||||
"cleanupBulkSsh_other": "{{count}} ŕēḿōţē ţàśķ ďĩŕēćţōŕĩēś ŵĩĺĺ ƀē ŕēḿōvēď (ƀēśţ-ēƒƒōŕţ).",
|
||||
"cleanupBulkSsh_one": "{{count}} ŕēḿōţē ţàśķ ďĩŕēćţōŕŷ ŵĩĺĺ ƀē ŕēḿōvēď.",
|
||||
"cleanupBulkSsh_other": "{{count}} ŕēḿōţē ţàśķ ďĩŕēćţōŕĩēś ŵĩĺĺ ƀē ŕēḿōvēď.",
|
||||
"cleanupBulkSshNote": "Ɓēśţ-ēƒƒōŕţ ŕēḿōvàĺ ḿàŷ ńōţ ćōḿƥĺēţē. Ŷōũŕ ĺōćàĺ ŕēƥō ĩś ńōţ ţōũćĥēď.",
|
||||
"cleanupBulkWorktree_one": "{{count}} ŵōŕķţŕēē àńď ĩţś ƀŕàńćĥ ŵĩĺĺ ƀē ďēĺēţēď.",
|
||||
"cleanupBulkWorktree_other": "{{count}} ŵōŕķţŕēēś àńď ţĥēĩŕ ƀŕàńćĥēś ŵĩĺĺ ƀē ďēĺēţēď.",
|
||||
"cleanupSingleLocal": "Ţĥē àĝēńţ ŕàń ďĩŕēćţĺŷ ĩń ŷōũŕ ŕēƥō - ŷōũŕ ƒĩĺēś, ƀŕàńćĥ, àńď ƒōĺďēŕ àŕē ńōţ ţōũćĥēď.",
|
||||
"cleanupSingleLocalDocker": "Ţĥē Ďōćķēŕ ćōńţàĩńēŕ ŕũńńĩńĝ ţĥĩś ţàśķ ŵĩĺĺ ƀē śţōƥƥēď àńď ŕēḿōvēď. Ŷōũŕ ĥōśţ ŕēƥō ĩś ńōţ ţōũćĥēď.",
|
||||
"cleanupBulkWorktreeNote": "Ŷōũŕ ḿàĩń ŕēƥō àńď ōţĥēŕ ƀŕàńćĥēś àŕē ńōţ àƒƒēćţēď.",
|
||||
"cleanupBulkLocalDockerNote": "Ŷōũŕ ĥōśţ ŕēƥō ĩś ńōţ ţōũćĥēď.",
|
||||
"cleanupSingleLocal": "Ţĥē àĝēńţ ŕàń ďĩŕēćţĺŷ ĩń ŷōũŕ ŕēƥō. Ŷōũŕ ƒĩĺēś, ƀŕàńćĥ, àńď ƒōĺďēŕ àŕē ńōţ ţōũćĥēď.",
|
||||
"cleanupSingleLocalDocker": "Ţĥē Ďōćķēŕ ćōńţàĩńēŕ ŕũńńĩńĝ ţĥĩś ţàśķ ŵĩĺĺ ƀē śţōƥƥēď àńď ŕēḿōvēď.",
|
||||
"cleanupSingleLocalDockerNote": "Ŷōũŕ ĥōśţ ŕēƥō ĩś ńōţ ţōũćĥēď.",
|
||||
"cleanupSingleRemoteDocker": "Ţĥē ŕēḿōţē Ďōćķēŕ ćōńţàĩńēŕ ŕũńńĩńĝ ţĥĩś ţàśķ ŵĩĺĺ ƀē śţōƥƥēď àńď ŕēḿōvēď.",
|
||||
"cleanupSingleSprites": "Ţĥē Śƥŕĩţēś ćĺōũď śàńďƀōx ƒōŕ ţĥĩś ţàśķ ŵĩĺĺ ƀē ďēśţŕōŷēď. Àńŷ ũńćōḿḿĩţţēď ŵōŕķ ĩńśĩďē ĩţ ŵĩĺĺ ƀē ĺōśţ.",
|
||||
"cleanupSingleSsh": "Ţĥē ţàśķ ďĩŕēćţōŕŷ ōń ţĥē ŕēḿōţē ĥōśţ ŵĩĺĺ ƀē ŕēḿōvēď (ƀēśţ-ēƒƒōŕţ). Ŷōũŕ ĺōćàĺ ŕēƥō ĩś ńōţ ţōũćĥēď.",
|
||||
"cleanupSingleWorktree": "Ţĥē ţàśķ'ś ĝĩţ ŵōŕķţŕēē àńď ĩţś ƀŕàńćĥ ŵĩĺĺ ƀē ďēĺēţēď. Ŷōũŕ ḿàĩń ŕēƥō àńď ōţĥēŕ ƀŕàńćĥēś àŕē ńōţ àƒƒēćţēď.",
|
||||
"cleanupSingleSsh": "Ţĥē ţàśķ ďĩŕēćţōŕŷ ōń ţĥē ŕēḿōţē ĥōśţ ŵĩĺĺ ƀē ŕēḿōvēď.",
|
||||
"cleanupSingleSshNote": "Ɓēśţ-ēƒƒōŕţ ŕēḿōvàĺ ḿàŷ ńōţ ćōḿƥĺēţē. Ŷōũŕ ĺōćàĺ ŕēƥō ĩś ńōţ ţōũćĥēď.",
|
||||
"cleanupSingleWorktree": "Ţĥē ţàśķ'ś ĝĩţ ŵōŕķţŕēē àńď ĩţś ƀŕàńćĥ ŵĩĺĺ ƀē ďēĺēţēď.",
|
||||
"cleanupSingleWorktreeNote": "Ŷōũŕ ḿàĩń ŕēƥō àńď ōţĥēŕ ƀŕàńćĥēś àŕē ńōţ àƒƒēćţēď.",
|
||||
"enhancedPromptCopiedToClipboard": "Ēńĥàńćēď ƥŕōḿƥţ ćōƥĩēď ţō ćĺĩƥƀōàŕď.",
|
||||
"enhancedPromptCouldNotBeCopied": "Ēńĥàńćēď ƥŕōḿƥţ ćōũĺď ńōţ ƀē ćōƥĩēď.",
|
||||
"enhancedPromptCouldNotBeInserted": "Ēńĥàńćēď ƥŕōḿƥţ ŵàś ĝēńēŕàţēď ƀũţ ćōũĺď ńōţ ƀē ĩńśēŕţēď.",
|
||||
|
||||
@@ -102,10 +102,10 @@
|
||||
"approvedVerb": "Aprovou",
|
||||
"approvers": "Aprovadores",
|
||||
"archive": "Arquivar",
|
||||
"archiveTaskConfirm": "Tem a certeza de que quer arquivar \"{{taskTitle}}\"?",
|
||||
"archiveTaskConfirm": "Arquivar \"{{taskTitle}}\".",
|
||||
"archiveTaskTitle": "Arquivar a tarefa?",
|
||||
"archiveTasksConfirm_one": "Tem a certeza de que quer arquivar {{count}} tarefa?",
|
||||
"archiveTasksConfirm_other": "Tem a certeza de que quer arquivar {{count}} tarefas?",
|
||||
"archiveTasksConfirm_one": "Arquivar {{count}} tarefa.",
|
||||
"archiveTasksConfirm_other": "Arquivar {{count}} tarefas.",
|
||||
"archiveTasksTitle_one": "Arquivar {{count}} tarefa?",
|
||||
"archiveTasksTitle_other": "Arquivar {{count}} tarefas?",
|
||||
"archiveTasks_one": "Arquivar {{count}} tarefa",
|
||||
@@ -328,10 +328,10 @@
|
||||
"deleteItemsTitle_other": "Eliminar {{count}} itens?",
|
||||
"deleteLayoutConfirm": "Eliminar {{name}}?",
|
||||
"deleteSession": "Eliminar a sessão?",
|
||||
"deleteTaskConfirm": "Tem a certeza de que quer eliminar \"{{taskTitle}}\"? Esta ação não pode ser anulada.",
|
||||
"deleteTaskConfirm": "Eliminar \"{{taskTitle}}\". Esta ação não pode ser anulada.",
|
||||
"deleteTaskTitle": "Eliminar tarefa",
|
||||
"deleteTasksConfirm_one": "Tem a certeza de que quer eliminar {{count}} tarefa? Esta ação não pode ser anulada.",
|
||||
"deleteTasksConfirm_other": "Tem a certeza de que quer eliminar {{count}} tarefas? Esta ação não pode ser anulada.",
|
||||
"deleteTasksConfirm_one": "Eliminar {{count}} tarefa. Esta ação não pode ser anulada.",
|
||||
"deleteTasksConfirm_other": "Eliminar {{count}} tarefas. Esta ação não pode ser anulada.",
|
||||
"deleteTasksCount_one": "Eliminar {{count}} tarefa",
|
||||
"deleteTasksCount_other": "Eliminar {{count}} tarefas",
|
||||
"deleteTasksTitle_one": "Eliminar {{count}} tarefa",
|
||||
@@ -1732,24 +1732,30 @@
|
||||
"branchTooltipWillSwitchTo": "O backend vai executar `git checkout` para mudar o seu repositório para este ramo antes de o agente arrancar.",
|
||||
"clarificationNoSelection": "Sem seleção",
|
||||
"cleanupAgentSessionsStopped": "Todas as sessões de agente em execução serão paradas.",
|
||||
"cleanupBulkLocal_one": "{{count}} tarefa local - as pastas do seu repositório não serão tocadas.",
|
||||
"cleanupBulkLocal_other": "{{count}} tarefas locais - as pastas do seu repositório não serão tocadas.",
|
||||
"cleanupBulkLocal_one": "{{count}} tarefa local executada diretamente no seu repositório. As pastas do seu repositório não serão tocadas.",
|
||||
"cleanupBulkLocal_other": "{{count}} tarefas locais executadas diretamente no seu repositório. As pastas do seu repositório não serão tocadas.",
|
||||
"cleanupBulkLocalDocker_one": "{{count}} contentor Docker será removido.",
|
||||
"cleanupBulkLocalDocker_other": "{{count}} contentores Docker serão removidos.",
|
||||
"cleanupBulkRemoteDocker_one": "{{count}} contentor Docker remoto será removido.",
|
||||
"cleanupBulkRemoteDocker_other": "{{count}} contentores Docker remotos serão removidos.",
|
||||
"cleanupBulkSprites_one": "{{count}} sandbox Sprites será destruída (trabalho não commitado perdido).",
|
||||
"cleanupBulkSprites_other": "{{count}} sandboxes Sprites serão destruídas (trabalho não commitado perdido).",
|
||||
"cleanupBulkSsh_one": "{{count}} diretório de tarefa remoto será removido (na medida do possível).",
|
||||
"cleanupBulkSsh_other": "{{count}} diretórios de tarefa remotos serão removidos (na medida do possível).",
|
||||
"cleanupBulkSsh_one": "{{count}} diretório de tarefa remoto será removido.",
|
||||
"cleanupBulkSsh_other": "{{count}} diretórios de tarefa remotos serão removidos.",
|
||||
"cleanupBulkSshNote": "A remoção é uma tentativa e pode não ser concluída. O seu repositório local não é tocado.",
|
||||
"cleanupBulkWorktree_one": "{{count}} worktree e o seu ramo serão eliminados.",
|
||||
"cleanupBulkWorktree_other": "{{count}} worktrees e os seus ramos serão eliminados.",
|
||||
"cleanupSingleLocal": "O agente correu diretamente no seu repositório - os seus ficheiros, ramo e pasta não são tocados.",
|
||||
"cleanupSingleLocalDocker": "O contentor Docker que corre esta tarefa será parado e removido. O repositório no seu anfitrião não é tocado.",
|
||||
"cleanupBulkWorktreeNote": "O seu repositório principal e os outros ramos não são afetados.",
|
||||
"cleanupBulkLocalDockerNote": "O repositório no seu anfitrião não é tocado.",
|
||||
"cleanupSingleLocal": "O agente correu diretamente no seu repositório. Os seus ficheiros, ramo e pasta não são tocados.",
|
||||
"cleanupSingleLocalDocker": "O contentor Docker que corre esta tarefa será parado e removido.",
|
||||
"cleanupSingleLocalDockerNote": "O repositório no seu anfitrião não é tocado.",
|
||||
"cleanupSingleRemoteDocker": "O contentor Docker remoto que corre esta tarefa será parado e removido.",
|
||||
"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 (na medida do possível). O seu repositório local não é tocado.",
|
||||
"cleanupSingleWorktree": "O worktree git da tarefa e o seu ramo serão eliminados. O seu repositório principal e os outros ramos não são afetados.",
|
||||
"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.",
|
||||
"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...",
|
||||
"composerPlaceholderClarification": "Colocar instruções na fila enquanto a pergunta está pendente...",
|
||||
"composerPlaceholderFile": "Continuar a trabalhar no ficheiro...",
|
||||
|
||||
@@ -102,10 +102,10 @@
|
||||
"approvedVerb": "已批准",
|
||||
"approvers": "审批人",
|
||||
"archive": "归档",
|
||||
"archiveTaskConfirm": "确定要归档“{{taskTitle}}”吗?",
|
||||
"archiveTaskConfirm": "归档“{{taskTitle}}”。",
|
||||
"archiveTaskTitle": "归档任务?",
|
||||
"archiveTasksConfirm_one": "确定要归档 {{count}} 个任务吗?",
|
||||
"archiveTasksConfirm_other": "确定要归档 {{count}} 个任务吗?",
|
||||
"archiveTasksConfirm_one": "归档 {{count}} 个任务。",
|
||||
"archiveTasksConfirm_other": "归档 {{count}} 个任务。",
|
||||
"archiveTasksTitle_one": "归档 {{count}} 个任务?",
|
||||
"archiveTasksTitle_other": "归档 {{count}} 个任务?",
|
||||
"archiveTasks_one": "归档 {{count}} 个任务",
|
||||
@@ -328,10 +328,10 @@
|
||||
"deleteItemsTitle_other": "删除 {{count}} 项?",
|
||||
"deleteLayoutConfirm": "删除 {{name}}?",
|
||||
"deleteSession": "删除会话?",
|
||||
"deleteTaskConfirm": "确定要删除“{{taskTitle}}”吗?此操作无法撤销。",
|
||||
"deleteTaskConfirm": "删除“{{taskTitle}}”。此操作无法撤销。",
|
||||
"deleteTaskTitle": "删除任务",
|
||||
"deleteTasksConfirm_one": "确定要删除 {{count}} 个任务吗?此操作无法撤销。",
|
||||
"deleteTasksConfirm_other": "确定要删除 {{count}} 个任务吗?此操作无法撤销。",
|
||||
"deleteTasksConfirm_one": "删除 {{count}} 个任务。此操作无法撤销。",
|
||||
"deleteTasksConfirm_other": "删除 {{count}} 个任务。此操作无法撤销。",
|
||||
"deleteTasksCount_one": "删除 {{count}} 个任务",
|
||||
"deleteTasksCount_other": "删除 {{count}} 个任务",
|
||||
"deleteTasksTitle_one": "删除 {{count}} 个任务",
|
||||
@@ -1732,24 +1732,30 @@
|
||||
"branchTooltipWillSwitchTo": "在智能体启动前,后端会执行 `git checkout` 将你的仓库切换到该分支。",
|
||||
"clarificationNoSelection": "未选择",
|
||||
"cleanupAgentSessionsStopped": "所有正在运行的智能体会话都将被停止。",
|
||||
"cleanupBulkLocal_one": "{{count}} 个本地任务 - 你的仓库目录不会被改动。",
|
||||
"cleanupBulkLocal_other": "{{count}} 个本地任务 - 你的仓库目录不会被改动。",
|
||||
"cleanupBulkLocal_one": "{{count}} 个本地任务直接在你的仓库中运行。你的仓库目录不会被改动。",
|
||||
"cleanupBulkLocal_other": "{{count}} 个本地任务直接在你的仓库中运行。你的仓库目录不会被改动。",
|
||||
"cleanupBulkLocalDocker_one": "将移除 {{count}} 个 Docker 容器。",
|
||||
"cleanupBulkLocalDocker_other": "将移除 {{count}} 个 Docker 容器。",
|
||||
"cleanupBulkRemoteDocker_one": "将移除 {{count}} 个远程 Docker 容器。",
|
||||
"cleanupBulkRemoteDocker_other": "将移除 {{count}} 个远程 Docker 容器。",
|
||||
"cleanupBulkSprites_one": "将销毁 {{count}} 个 Sprites 沙箱(未提交的工作将丢失)。",
|
||||
"cleanupBulkSprites_other": "将销毁 {{count}} 个 Sprites 沙箱(未提交的工作将丢失)。",
|
||||
"cleanupBulkSsh_one": "将尽力移除 {{count}} 个远程任务目录。",
|
||||
"cleanupBulkSsh_other": "将尽力移除 {{count}} 个远程任务目录。",
|
||||
"cleanupBulkSsh_one": "将移除 {{count}} 个远程任务目录。",
|
||||
"cleanupBulkSsh_other": "将移除 {{count}} 个远程任务目录。",
|
||||
"cleanupBulkSshNote": "尽力移除,可能无法完成。你的本地仓库不会被改动。",
|
||||
"cleanupBulkWorktree_one": "将删除 {{count}} 个 worktree 及其分支。",
|
||||
"cleanupBulkWorktree_other": "将删除 {{count}} 个 worktree 及其分支。",
|
||||
"cleanupSingleLocal": "智能体直接在你的仓库中运行 - 你的文件、分支和目录都不会被改动。",
|
||||
"cleanupSingleLocalDocker": "运行该任务的 Docker 容器将被停止并移除。你主机上的仓库不会被改动。",
|
||||
"cleanupBulkWorktreeNote": "你的主仓库和其他分支不受影响。",
|
||||
"cleanupBulkLocalDockerNote": "你主机上的仓库不会被改动。",
|
||||
"cleanupSingleLocal": "智能体直接在你的仓库中运行。你的文件、分支和目录都不会被改动。",
|
||||
"cleanupSingleLocalDocker": "运行该任务的 Docker 容器将被停止并移除。",
|
||||
"cleanupSingleLocalDockerNote": "你主机上的仓库不会被改动。",
|
||||
"cleanupSingleRemoteDocker": "运行该任务的远程 Docker 容器将被停止并移除。",
|
||||
"cleanupSingleSprites": "该任务的 Sprites 云沙箱将被销毁。其中所有未提交的工作都会丢失。",
|
||||
"cleanupSingleSsh": "远程主机上的任务目录将被尽力移除。你的本地仓库不会被改动。",
|
||||
"cleanupSingleWorktree": "该任务的 git worktree 及其分支将被删除。你的主仓库和其他分支不受影响。",
|
||||
"cleanupSingleSsh": "远程主机上的任务目录将被移除。",
|
||||
"cleanupSingleSshNote": "尽力移除,可能无法完成。你的本地仓库不会被改动。",
|
||||
"cleanupSingleWorktree": "该任务的 git worktree 及其分支将被删除。",
|
||||
"cleanupSingleWorktreeNote": "你的主仓库和其他分支不受影响。",
|
||||
"composerPlaceholderBusy": "为智能体排队指令…",
|
||||
"composerPlaceholderClarification": "在问题待答期间排队指令…",
|
||||
"composerPlaceholderFile": "继续处理该文件…",
|
||||
|
||||
@@ -102,10 +102,10 @@
|
||||
"approvedVerb": "已批准",
|
||||
"approvers": "審批人",
|
||||
"archive": "歸檔",
|
||||
"archiveTaskConfirm": "確定要歸檔「{{taskTitle}}」嗎?",
|
||||
"archiveTaskConfirm": "歸檔「{{taskTitle}}」。",
|
||||
"archiveTaskTitle": "歸檔任務?",
|
||||
"archiveTasksConfirm_one": "確定要歸檔 {{count}} 個任務嗎?",
|
||||
"archiveTasksConfirm_other": "確定要歸檔 {{count}} 個任務嗎?",
|
||||
"archiveTasksConfirm_one": "歸檔 {{count}} 個任務。",
|
||||
"archiveTasksConfirm_other": "歸檔 {{count}} 個任務。",
|
||||
"archiveTasksTitle_one": "歸檔 {{count}} 個任務?",
|
||||
"archiveTasksTitle_other": "歸檔 {{count}} 個任務?",
|
||||
"archiveTasks_one": "歸檔 {{count}} 個任務",
|
||||
@@ -328,10 +328,10 @@
|
||||
"deleteItemsTitle_other": "刪除 {{count}} 項?",
|
||||
"deleteLayoutConfirm": "刪除 {{name}}?",
|
||||
"deleteSession": "刪除工作階段?",
|
||||
"deleteTaskConfirm": "確定要刪除「{{taskTitle}}」嗎?此操作無法撤銷。",
|
||||
"deleteTaskConfirm": "刪除「{{taskTitle}}」。此操作無法撤銷。",
|
||||
"deleteTaskTitle": "刪除任務",
|
||||
"deleteTasksConfirm_one": "確定要刪除 {{count}} 個任務嗎?此操作無法撤銷。",
|
||||
"deleteTasksConfirm_other": "確定要刪除 {{count}} 個任務嗎?此操作無法撤銷。",
|
||||
"deleteTasksConfirm_one": "刪除 {{count}} 個任務。此操作無法撤銷。",
|
||||
"deleteTasksConfirm_other": "刪除 {{count}} 個任務。此操作無法撤銷。",
|
||||
"deleteTasksCount_one": "刪除 {{count}} 個任務",
|
||||
"deleteTasksCount_other": "刪除 {{count}} 個任務",
|
||||
"deleteTasksTitle_one": "刪除 {{count}} 個任務",
|
||||
@@ -1732,24 +1732,30 @@
|
||||
"branchTooltipWillSwitchTo": "在代理程式啓動前,後端會執行 `git checkout` 將你的儲存庫切換到該分支。",
|
||||
"clarificationNoSelection": "未選擇",
|
||||
"cleanupAgentSessionsStopped": "所有正在執行的代理程式工作階段都將被停止。",
|
||||
"cleanupBulkLocal_one": "{{count}} 個本地任務 - 你的儲存庫目錄不會被改動。",
|
||||
"cleanupBulkLocal_other": "{{count}} 個本地任務 - 你的儲存庫目錄不會被改動。",
|
||||
"cleanupBulkLocal_one": "{{count}} 個本地任務直接在你的儲存庫中執行。你的儲存庫目錄不會被改動。",
|
||||
"cleanupBulkLocal_other": "{{count}} 個本地任務直接在你的儲存庫中執行。你的儲存庫目錄不會被改動。",
|
||||
"cleanupBulkLocalDocker_one": "將移除 {{count}} 個 Docker 容器。",
|
||||
"cleanupBulkLocalDocker_other": "將移除 {{count}} 個 Docker 容器。",
|
||||
"cleanupBulkRemoteDocker_one": "將移除 {{count}} 個遠端 Docker 容器。",
|
||||
"cleanupBulkRemoteDocker_other": "將移除 {{count}} 個遠端 Docker 容器。",
|
||||
"cleanupBulkSprites_one": "將銷燬 {{count}} 個 Sprites 沙箱(未提交的工作將丟失)。",
|
||||
"cleanupBulkSprites_other": "將銷燬 {{count}} 個 Sprites 沙箱(未提交的工作將丟失)。",
|
||||
"cleanupBulkSsh_one": "將盡力移除 {{count}} 個遠端任務目錄。",
|
||||
"cleanupBulkSsh_other": "將盡力移除 {{count}} 個遠端任務目錄。",
|
||||
"cleanupBulkSsh_one": "將移除 {{count}} 個遠端任務目錄。",
|
||||
"cleanupBulkSsh_other": "將移除 {{count}} 個遠端任務目錄。",
|
||||
"cleanupBulkSshNote": "盡力移除,可能無法完成。你的本地儲存庫不會被改動。",
|
||||
"cleanupBulkWorktree_one": "將刪除 {{count}} 個 worktree 及其分支。",
|
||||
"cleanupBulkWorktree_other": "將刪除 {{count}} 個 worktree 及其分支。",
|
||||
"cleanupSingleLocal": "代理程式直接在你的儲存庫中執行 - 你的檔案、分支和目錄都不會被改動。",
|
||||
"cleanupSingleLocalDocker": "執行該任務的 Docker 容器將被停止並移除。你主機上的儲存庫不會被改動。",
|
||||
"cleanupBulkWorktreeNote": "你的主儲存庫和其他分支不受影響。",
|
||||
"cleanupBulkLocalDockerNote": "你主機上的儲存庫不會被改動。",
|
||||
"cleanupSingleLocal": "代理程式直接在你的儲存庫中執行。你的檔案、分支和目錄都不會被改動。",
|
||||
"cleanupSingleLocalDocker": "執行該任務的 Docker 容器將被停止並移除。",
|
||||
"cleanupSingleLocalDockerNote": "你主機上的儲存庫不會被改動。",
|
||||
"cleanupSingleRemoteDocker": "執行該任務的遠端 Docker 容器將被停止並移除。",
|
||||
"cleanupSingleSprites": "該任務的 Sprites 雲沙箱將被銷燬。其中所有未提交的工作都會丟失。",
|
||||
"cleanupSingleSsh": "遠端主機上的任務目錄將被盡力移除。你的本地儲存庫不會被改動。",
|
||||
"cleanupSingleWorktree": "該任務的 git worktree 及其分支將被刪除。你的主儲存庫和其他分支不受影響。",
|
||||
"cleanupSingleSsh": "遠端主機上的任務目錄將被移除。",
|
||||
"cleanupSingleSshNote": "盡力移除,可能無法完成。你的本地儲存庫不會被改動。",
|
||||
"cleanupSingleWorktree": "該任務的 git worktree 及其分支將被刪除。",
|
||||
"cleanupSingleWorktreeNote": "你的主儲存庫和其他分支不受影響。",
|
||||
"composerPlaceholderBusy": "為代理程式排隊指令…",
|
||||
"composerPlaceholderClarification": "在問題待答期間排隊指令…",
|
||||
"composerPlaceholderFile": "繼續處理該檔案…",
|
||||
|
||||
@@ -102,10 +102,10 @@
|
||||
"approvedVerb": "已批准",
|
||||
"approvers": "審批人",
|
||||
"archive": "歸檔",
|
||||
"archiveTaskConfirm": "確定要歸檔「{{taskTitle}}」嗎?",
|
||||
"archiveTaskConfirm": "歸檔「{{taskTitle}}」。",
|
||||
"archiveTaskTitle": "歸檔任務?",
|
||||
"archiveTasksConfirm_one": "確定要歸檔 {{count}} 個任務嗎?",
|
||||
"archiveTasksConfirm_other": "確定要歸檔 {{count}} 個任務嗎?",
|
||||
"archiveTasksConfirm_one": "歸檔 {{count}} 個任務。",
|
||||
"archiveTasksConfirm_other": "歸檔 {{count}} 個任務。",
|
||||
"archiveTasksTitle_one": "歸檔 {{count}} 個任務?",
|
||||
"archiveTasksTitle_other": "歸檔 {{count}} 個任務?",
|
||||
"archiveTasks_one": "歸檔 {{count}} 個任務",
|
||||
@@ -328,10 +328,10 @@
|
||||
"deleteItemsTitle_other": "刪除 {{count}} 項?",
|
||||
"deleteLayoutConfirm": "刪除 {{name}}?",
|
||||
"deleteSession": "刪除工作階段?",
|
||||
"deleteTaskConfirm": "確定要刪除「{{taskTitle}}」嗎?此操作無法撤銷。",
|
||||
"deleteTaskConfirm": "刪除「{{taskTitle}}」。此操作無法撤銷。",
|
||||
"deleteTaskTitle": "刪除任務",
|
||||
"deleteTasksConfirm_one": "確定要刪除 {{count}} 個任務嗎?此操作無法撤銷。",
|
||||
"deleteTasksConfirm_other": "確定要刪除 {{count}} 個任務嗎?此操作無法撤銷。",
|
||||
"deleteTasksConfirm_one": "刪除 {{count}} 個任務。此操作無法撤銷。",
|
||||
"deleteTasksConfirm_other": "刪除 {{count}} 個任務。此操作無法撤銷。",
|
||||
"deleteTasksCount_one": "刪除 {{count}} 個任務",
|
||||
"deleteTasksCount_other": "刪除 {{count}} 個任務",
|
||||
"deleteTasksTitle_one": "刪除 {{count}} 個任務",
|
||||
@@ -1732,24 +1732,30 @@
|
||||
"branchTooltipWillSwitchTo": "在代理程式啟動前,後端會執行 `git checkout` 將你的儲存庫切換到該分支。",
|
||||
"clarificationNoSelection": "未選擇",
|
||||
"cleanupAgentSessionsStopped": "所有正在執行的代理程式工作階段都將被停止。",
|
||||
"cleanupBulkLocal_one": "{{count}} 個本地任務 - 你的儲存庫目錄不會被改動。",
|
||||
"cleanupBulkLocal_other": "{{count}} 個本地任務 - 你的儲存庫目錄不會被改動。",
|
||||
"cleanupBulkLocal_one": "{{count}} 個本地任務直接在你的儲存庫中執行。你的儲存庫目錄不會被改動。",
|
||||
"cleanupBulkLocal_other": "{{count}} 個本地任務直接在你的儲存庫中執行。你的儲存庫目錄不會被改動。",
|
||||
"cleanupBulkLocalDocker_one": "將移除 {{count}} 個 Docker 容器。",
|
||||
"cleanupBulkLocalDocker_other": "將移除 {{count}} 個 Docker 容器。",
|
||||
"cleanupBulkRemoteDocker_one": "將移除 {{count}} 個遠端 Docker 容器。",
|
||||
"cleanupBulkRemoteDocker_other": "將移除 {{count}} 個遠端 Docker 容器。",
|
||||
"cleanupBulkSprites_one": "將銷燬 {{count}} 個 Sprites 沙箱(未提交的工作將丟失)。",
|
||||
"cleanupBulkSprites_other": "將銷燬 {{count}} 個 Sprites 沙箱(未提交的工作將丟失)。",
|
||||
"cleanupBulkSsh_one": "將盡力移除 {{count}} 個遠端任務目錄。",
|
||||
"cleanupBulkSsh_other": "將盡力移除 {{count}} 個遠端任務目錄。",
|
||||
"cleanupBulkSsh_one": "將移除 {{count}} 個遠端任務目錄。",
|
||||
"cleanupBulkSsh_other": "將移除 {{count}} 個遠端任務目錄。",
|
||||
"cleanupBulkSshNote": "盡力移除,可能無法完成。你的本地儲存庫不會被改動。",
|
||||
"cleanupBulkWorktree_one": "將刪除 {{count}} 個 worktree 及其分支。",
|
||||
"cleanupBulkWorktree_other": "將刪除 {{count}} 個 worktree 及其分支。",
|
||||
"cleanupSingleLocal": "代理程式直接在你的儲存庫中執行 - 你的檔案、分支和目錄都不會被改動。",
|
||||
"cleanupSingleLocalDocker": "執行該任務的 Docker 容器將被停止並移除。你主機上的儲存庫不會被改動。",
|
||||
"cleanupBulkWorktreeNote": "你的主儲存庫和其他分支不受影響。",
|
||||
"cleanupBulkLocalDockerNote": "你主機上的儲存庫不會被改動。",
|
||||
"cleanupSingleLocal": "代理程式直接在你的儲存庫中執行。你的檔案、分支和目錄都不會被改動。",
|
||||
"cleanupSingleLocalDocker": "執行該任務的 Docker 容器將被停止並移除。",
|
||||
"cleanupSingleLocalDockerNote": "你主機上的儲存庫不會被改動。",
|
||||
"cleanupSingleRemoteDocker": "執行該任務的遠端 Docker 容器將被停止並移除。",
|
||||
"cleanupSingleSprites": "該任務的 Sprites 雲沙箱將被銷燬。其中所有未提交的工作都會丟失。",
|
||||
"cleanupSingleSsh": "遠端主機上的任務目錄將被盡力移除。你的本地儲存庫不會被改動。",
|
||||
"cleanupSingleWorktree": "該任務的 git worktree 及其分支將被刪除。你的主儲存庫和其他分支不受影響。",
|
||||
"cleanupSingleSsh": "遠端主機上的任務目錄將被移除。",
|
||||
"cleanupSingleSshNote": "盡力移除,可能無法完成。你的本地儲存庫不會被改動。",
|
||||
"cleanupSingleWorktree": "該任務的 git worktree 及其分支將被刪除。",
|
||||
"cleanupSingleWorktreeNote": "你的主儲存庫和其他分支不受影響。",
|
||||
"composerPlaceholderBusy": "為代理程式排隊指令…",
|
||||
"composerPlaceholderClarification": "在問題待答期間排隊指令…",
|
||||
"composerPlaceholderFile": "繼續處理該檔案…",
|
||||
|
||||
@@ -105,7 +105,7 @@ the final composition.
|
||||
|
||||
- [x] [Task 01: Standardize surface typography primitives](task-01-standardize-surface-typography-primitives.md) (completed)
|
||||
- [x] [Task 02: Normalize structured app confirmations](task-02-normalize-structured-app-confirmations.md) (completed)
|
||||
- [ ] [Task 03: Refine task cleanup confirmations](task-03-refine-task-cleanup-confirmations.md)
|
||||
- [x] [Task 03: Refine task cleanup confirmations](task-03-refine-task-cleanup-confirmations.md) (completed)
|
||||
- [ ] [Task 04: Normalize structured task confirmations](task-04-normalize-structured-task-confirmations.md)
|
||||
- [ ] [Task 05: Add responsive text hierarchy E2E](task-05-add-responsive-text-hierarchy-e2e.md)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: "03-refine-task-cleanup-confirmations"
|
||||
title: "Refine task cleanup confirmations"
|
||||
status: pending
|
||||
status: completed
|
||||
wave: 2
|
||||
depends_on:
|
||||
- "01-standardize-surface-typography-primitives"
|
||||
@@ -124,4 +124,22 @@ pnpm run i18n:ratchet
|
||||
|
||||
## Results
|
||||
|
||||
Pending implementation.
|
||||
- RED: The four focused suites failed 22 tests against the pre-change flat
|
||||
cleanup-line model, prose-only markup, unconstrained dialog body, compact
|
||||
phone actions, and non-semantic Delete styling.
|
||||
- GREEN: Replaced cleanup `lines` with ordered effects and supporting notes,
|
||||
added one semantic renderer shared by full and compact archive/delete
|
||||
surfaces, and preserved every executor-specific cleanup fact and callback.
|
||||
- Added dynamic-viewport body containment, persistent full-dialog footers,
|
||||
full-width 44px phone actions with compact desktop sizing, and the semantic
|
||||
destructive Delete variant.
|
||||
- Updated all five task catalogs with direct outcome, ordered effect, and
|
||||
reassurance copy while preserving plural placeholders and locale parity.
|
||||
- Focused verification passed with 4 test files and 63 tests. Web typecheck,
|
||||
focused ESLint with `--max-warnings 0`, `pnpm run i18n:check`, and
|
||||
`pnpm run i18n:ratchet` also passed on the integrated stack.
|
||||
- The supported task-scoped Traditional Chinese generation command passed with
|
||||
zero residuals for `zh-tw` and `zh-hk`; it produced no owned-key difference.
|
||||
The repository-wide `pnpm run i18n:zh-hant` command still stops before any
|
||||
write on two inherited `agents:dynamicProfileSettings` residuals, reproduced
|
||||
unchanged on the exact Task 02 parent.
|
||||
|
||||
Reference in New Issue
Block a user