fix(web): refine task cleanup confirmations (#3231)

This commit is contained in:
José Almeida
2026-09-01 14:15:24 +00:00
committed by GitHub
parent 9fa6a4d3ad
commit f46ff3b276
19 changed files with 606 additions and 291 deletions
@@ -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;
+20 -14
View File
@@ -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.",
+20 -14
View File
@@ -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": "Ēńĥàńćēď ƥŕōḿƥţ ŵàś ĝēńēŕàţēď ƀũţ ćōũĺď ńōţ ƀē ĩńśēŕţēď.",
+20 -14
View File
@@ -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...",
+20 -14
View File
@@ -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": "继续处理该文件…",
+20 -14
View File
@@ -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": "繼續處理該檔案…",
+20 -14
View File
@@ -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": "繼續處理該檔案…",
+1 -1
View File
@@ -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.