From f46ff3b27615efe516cd4eed9aa015028cfcf915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Almeida?= <57680069+zeval@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:15:24 +0000 Subject: [PATCH] fix(web): refine task cleanup confirmations (#3231) --- .../task/chat/pr-archive-banners.test.tsx | 8 +- .../task/task-archive-confirm-dialog.test.tsx | 62 ++++++- .../task/task-archive-confirm-dialog.tsx | 61 ++++--- .../task/task-archive-confirmation.test.tsx | 39 +++- .../task/task-archive-confirmation.tsx | 31 ++-- .../task/task-cleanup-consequences.tsx | 65 +++++++ .../task/task-cleanup-summary.test.ts | 172 ++++++++++-------- .../components/task/task-cleanup-summary.ts | 111 ++++++----- .../task/task-confirm-dialog-shared.ts | 8 +- .../task/task-delete-confirm-dialog.test.tsx | 49 +++++ .../task/task-delete-confirm-dialog.tsx | 63 ++++--- apps/web/src/locales/en/task.json | 34 ++-- apps/web/src/locales/pseudo/task.json | 34 ++-- apps/web/src/locales/pt-pt/task.json | 34 ++-- apps/web/src/locales/zh-cn/task.json | 34 ++-- apps/web/src/locales/zh-hk/task.json | 34 ++-- apps/web/src/locales/zh-tw/task.json | 34 ++-- docs/plans/surface-text-hierarchy/plan.md | 2 +- ...sk-03-refine-task-cleanup-confirmations.md | 22 ++- 19 files changed, 606 insertions(+), 291 deletions(-) create mode 100644 apps/web/components/task/task-cleanup-consequences.tsx diff --git a/apps/web/components/task/chat/pr-archive-banners.test.tsx b/apps/web/components/task/chat/pr-archive-banners.test.tsx index 0eea4c5b7e..76ec9fcc1a 100644 --- a/apps/web/components/task/chat/pr-archive-banners.test.tsx +++ b/apps/web/components/task/chat/pr-archive-banners.test.tsx @@ -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, + ); }); }); diff --git a/apps/web/components/task/task-archive-confirm-dialog.test.tsx b/apps/web/components/task/task-archive-confirm-dialog.test.tsx index 6d115e65d7..b063470a66 100644 --- a/apps/web/components/task/task-archive-confirm-dialog.test.tsx +++ b/apps/web/components/task/task-archive-confirm-dialog.test.tsx @@ -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( + {}} + 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( + {}} + 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( diff --git a/apps/web/components/task/task-archive-confirm-dialog.tsx b/apps/web/components/task/task-archive-confirm-dialog.tsx index 56ac43feda..16b4ed9b47 100644 --- a/apps/web/components/task/task-archive-confirm-dialog.tsx +++ b/apps/web/components/task/task-archive-confirm-dialog.tsx @@ -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({ {title} - -
-

{firstLine}

- {cleanup.lines.map((line, i) => ( -

- {line} -

- ))} + +
+ +
+

{firstLine}

+
- - {taskIsInFlight && } - {subtaskCount > 0 && ( - - )} - - + + )} +
+ + {t("common:cancel")} { if (archiveDisabled) return; diff --git a/apps/web/components/task/task-archive-confirmation.test.tsx b/apps/web/components/task/task-archive-confirmation.test.tsx index f826873df6..375d420636 100644 --- a/apps/web/components/task/task-archive-confirmation.test.tsx +++ b/apps/web/components/task/task-archive-confirmation.test.tsx @@ -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"); + }); +}); diff --git a/apps/web/components/task/task-archive-confirmation.tsx b/apps/web/components/task/task-archive-confirmation.tsx index ec973235b9..c16f814bb8 100644 --- a/apps/web/components/task/task-archive-confirmation.tsx +++ b/apps/web/components/task/task-archive-confirmation.tsx @@ -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 ( - {t("task:archiveTaskConfirm", { taskTitle })} - {cleanupLines.map((line) => ( - - {line} - - ))} + + {t("task:archiveTaskConfirm", { taskTitle })} + + {taskIsInFlight && } ); @@ -83,12 +82,9 @@ function ArchiveConfirmCopy({ confirmTestId: string; }) { const { t } = useTranslation(); + const cleanup = getCleanupSummary(executorType); const description = ( - + ); return ( + } cancelLabel={t("common:cancel")} confirmLabel={t(ARCHIVE_LABEL_KEY)} diff --git a/apps/web/components/task/task-cleanup-consequences.tsx b/apps/web/components/task/task-cleanup-consequences.tsx new file mode 100644 index 0000000000..28ac5d381a --- /dev/null +++ b/apps/web/components/task/task-cleanup-consequences.tsx @@ -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 ( + + + {summary.effects.map((effect, index) => ( + + {effect} + + ))} + + {summary.notes.length > 0 && ( + + {summary.notes.map((note, index) => ( + + {note} + + ))} + + )} + + ); + } + + return ( +
+
    + {summary.effects.map((effect, index) => ( +
  • + {effect} +
  • + ))} +
+ {summary.notes.length > 0 && ( +
+ {summary.notes.map((note, index) => ( +

+ {note} +

+ ))} +
+ )} +
+ ); +} diff --git a/apps/web/components/task/task-cleanup-summary.test.ts b/apps/web/components/task/task-cleanup-summary.test.ts index 4225e11c76..c85f76f0a3 100644 --- a/apps/web/components/task/task-cleanup-summary.test.ts +++ b/apps/web/components/task/task-cleanup-summary.test.ts @@ -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)."); }); }); diff --git a/apps/web/components/task/task-cleanup-summary.ts b/apps/web/components/task/task-cleanup-summary.ts index 3ce0003dea..f842f84bff 100644 --- a/apps/web/components/task/task-cleanup-summary.ts +++ b/apps/web/components/task/task-cleanup-summary.ts @@ -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 = { - 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 = { + 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 = { - 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 = { + 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, ): CleanupSummary { - const generic = t(GENERIC_KEY); - if (executorTypes.length === 0) return { lines: [generic] }; - const counts = new Map(); 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 }; } diff --git a/apps/web/components/task/task-confirm-dialog-shared.ts b/apps/web/components/task/task-confirm-dialog-shared.ts index 7b63b35fab..ef61ef0d56 100644 --- a/apps/web/components/task/task-confirm-dialog-shared.ts +++ b/apps/web/components/task/task-confirm-dialog-shared.ts @@ -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) => event.stopPropagation(); diff --git a/apps/web/components/task/task-delete-confirm-dialog.test.tsx b/apps/web/components/task/task-delete-confirm-dialog.test.tsx index ce6151a66b..ad41228f46 100644 --- a/apps/web/components/task/task-delete-confirm-dialog.test.tsx +++ b/apps/web/components/task/task-delete-confirm-dialog.test.tsx @@ -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( + {}} + 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( + {}} + 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( diff --git a/apps/web/components/task/task-delete-confirm-dialog.tsx b/apps/web/components/task/task-delete-confirm-dialog.tsx index 0fed0ca9e1..d0d3a51025 100644 --- a/apps/web/components/task/task-delete-confirm-dialog.tsx +++ b/apps/web/components/task/task-delete-confirm-dialog.tsx @@ -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({ {title} - -
-

{description}

- {cleanup.lines.map((line, i) => ( -

- {line} -

- ))} + +
+ +
+

{description}

+
- - {(isInFlight || storeInFlight) && ( - - )} - {subtaskCount > 0 && ( - - )} - - + + )} +
+ + {t("common:cancel")} { if (isDeleting) return; diff --git a/apps/web/src/locales/en/task.json b/apps/web/src/locales/en/task.json index fd355dd5d6..14cad60d8e 100644 --- a/apps/web/src/locales/en/task.json +++ b/apps/web/src/locales/en/task.json @@ -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.", diff --git a/apps/web/src/locales/pseudo/task.json b/apps/web/src/locales/pseudo/task.json index de7a32d0fa..d8baab5355 100644 --- a/apps/web/src/locales/pseudo/task.json +++ b/apps/web/src/locales/pseudo/task.json @@ -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": "Ēńĥàńćēď ƥŕōḿƥţ ŵàś ĝēńēŕàţēď ƀũţ ćōũĺď ńōţ ƀē ĩńśēŕţēď.", diff --git a/apps/web/src/locales/pt-pt/task.json b/apps/web/src/locales/pt-pt/task.json index 1d43338855..11666b7362 100644 --- a/apps/web/src/locales/pt-pt/task.json +++ b/apps/web/src/locales/pt-pt/task.json @@ -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...", diff --git a/apps/web/src/locales/zh-cn/task.json b/apps/web/src/locales/zh-cn/task.json index dfa0967a7b..3401a05f2c 100644 --- a/apps/web/src/locales/zh-cn/task.json +++ b/apps/web/src/locales/zh-cn/task.json @@ -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": "继续处理该文件…", diff --git a/apps/web/src/locales/zh-hk/task.json b/apps/web/src/locales/zh-hk/task.json index 7511bb562e..6154322647 100644 --- a/apps/web/src/locales/zh-hk/task.json +++ b/apps/web/src/locales/zh-hk/task.json @@ -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": "繼續處理該檔案…", diff --git a/apps/web/src/locales/zh-tw/task.json b/apps/web/src/locales/zh-tw/task.json index b54c711943..3e5236b788 100644 --- a/apps/web/src/locales/zh-tw/task.json +++ b/apps/web/src/locales/zh-tw/task.json @@ -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": "繼續處理該檔案…", diff --git a/docs/plans/surface-text-hierarchy/plan.md b/docs/plans/surface-text-hierarchy/plan.md index 758c91c0c8..b4d9f53342 100644 --- a/docs/plans/surface-text-hierarchy/plan.md +++ b/docs/plans/surface-text-hierarchy/plan.md @@ -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) diff --git a/docs/plans/surface-text-hierarchy/task-03-refine-task-cleanup-confirmations.md b/docs/plans/surface-text-hierarchy/task-03-refine-task-cleanup-confirmations.md index 68e18cccf0..a4387d8993 100644 --- a/docs/plans/surface-text-hierarchy/task-03-refine-task-cleanup-confirmations.md +++ b/docs/plans/surface-text-hierarchy/task-03-refine-task-cleanup-confirmations.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.