From 9fa6a4d3adc611b7985117203fa8fe5c11de12fe 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 13:57:55 +0000 Subject: [PATCH] fix: normalize structured app confirmations (#3220) --- .../quick-chat-delete-dialog.test.tsx | 46 +++++++++++++++++++ .../quick-chat/quick-chat-delete-dialog.tsx | 6 +-- .../agent-profile-delete-dialog.test.tsx | 22 +++++++++ .../settings/agent-profile-delete-dialog.tsx | 26 +++++------ docs/plans/surface-text-hierarchy/plan.md | 2 +- ...-normalize-structured-app-confirmations.md | 27 ++++++++++- 6 files changed, 110 insertions(+), 19 deletions(-) create mode 100644 apps/web/components/quick-chat/quick-chat-delete-dialog.test.tsx diff --git a/apps/web/components/quick-chat/quick-chat-delete-dialog.test.tsx b/apps/web/components/quick-chat/quick-chat-delete-dialog.test.tsx new file mode 100644 index 0000000000..b3607c7ba0 --- /dev/null +++ b/apps/web/components/quick-chat/quick-chat-delete-dialog.test.tsx @@ -0,0 +1,46 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react"; + +import { QuickChatDeleteDialog } from "./quick-chat-delete-dialog"; + +afterEach(cleanup); + +function renderDeleteDialog(onConfirm = vi.fn()) { + return render( + , + ); +} + +describe("QuickChatDeleteDialog structured description", () => { + // @covers AC-UI-SURFACE-TEXT-HIERARCHY-001.2 + it("keeps one left-aligned description boundary with semantic paragraphs and a list", () => { + renderDeleteDialog(); + + const dialog = screen.getByRole("alertdialog"); + const descriptions = dialog.querySelectorAll('[data-slot="alert-dialog-description"]'); + expect(descriptions).toHaveLength(1); + + const description = descriptions[0] as HTMLElement; + expect(description.tagName).toBe("DIV"); + expect(description.className).toContain("min-w-0"); + expect(description.className).toContain("text-left"); + expect(description.className).toContain("space-y-2"); + expect(description.querySelectorAll(":scope > p")).toHaveLength(2); + + const list = description.querySelector(":scope > ul"); + expect(list).not.toBeNull(); + expect(list?.querySelectorAll(":scope > li")).toHaveLength(3); + }); + + // @covers AC-UI-SURFACE-TEXT-HIERARCHY-001.5 + it("preserves the cancel and delete actions", async () => { + const onConfirm = vi.fn(); + renderDeleteDialog(onConfirm); + + const dialog = screen.getByRole("alertdialog"); + expect(within(dialog).getByRole("button", { name: "Cancel" })).toBeTruthy(); + fireEvent.click(within(dialog).getByRole("button", { name: "Delete" })); + + await waitFor(() => expect(onConfirm).toHaveBeenCalledTimes(1)); + }); +}); diff --git a/apps/web/components/quick-chat/quick-chat-delete-dialog.tsx b/apps/web/components/quick-chat/quick-chat-delete-dialog.tsx index f1f0227678..a3f18cc020 100644 --- a/apps/web/components/quick-chat/quick-chat-delete-dialog.tsx +++ b/apps/web/components/quick-chat/quick-chat-delete-dialog.tsx @@ -29,15 +29,15 @@ export function QuickChatDeleteDialog({ {t("chat:deleteQuickChat")} - +

{t("chat:deleteQuickChatIntro")}

-
    +
    • {t("chat:allConversationHistory")}
    • {t("chat:theTaskAndItsData")}
    • {t("chat:theAssociatedWorktree")}
    -

    {t("chat:thisActionCannotBeUndone")}

    +

    {t("chat:thisActionCannotBeUndone")}

diff --git a/apps/web/components/settings/agent-profile-delete-dialog.test.tsx b/apps/web/components/settings/agent-profile-delete-dialog.test.tsx index 76b3319dce..5e03462fa5 100644 --- a/apps/web/components/settings/agent-profile-delete-dialog.test.tsx +++ b/apps/web/components/settings/agent-profile-delete-dialog.test.tsx @@ -260,4 +260,26 @@ describe("AgentProfileDeleteConflictDialog layout", () => { expect(body.contains(footer)).toBe(false); expect(within(footer).getByRole("button", { name: "Cancel" })).toBeTruthy(); }); + + // @covers AC-UI-SURFACE-TEXT-HIERARCHY-001.2, AC-UI-SURFACE-TEXT-HIERARCHY-001.3 + it("keeps long conflict labels in one left-aligned semantic description", () => { + const longTaskTitle = `Profile task ${"x".repeat(320)}`; + renderConflictDialog({ + activeSessions: [{ task_id: "long-task", task_title: longTaskTitle, is_ephemeral: false }], + watchers: [], + routingTiers: [], + automations: [], + }); + + const body = screen.getByTestId("agent-profile-delete-conflict-body"); + expect(body.className).toContain("min-w-0"); + expect(body.className).toContain("text-left"); + expect(body.className).toContain("space-y-2"); + const directParagraphs = body.querySelectorAll(":scope > p"); + expect(directParagraphs).toHaveLength(2); + expect(directParagraphs[1]?.className).not.toContain("mt-2"); + + const taskItem = screen.getByText(longTaskTitle); + expect(taskItem.tagName).toBe("LI"); + }); }); diff --git a/apps/web/components/settings/agent-profile-delete-dialog.tsx b/apps/web/components/settings/agent-profile-delete-dialog.tsx index c730550a26..8dc961a1eb 100644 --- a/apps/web/components/settings/agent-profile-delete-dialog.tsx +++ b/apps/web/components/settings/agent-profile-delete-dialog.tsx @@ -184,7 +184,7 @@ export function AgentProfileDeleteConflictDialog({

{t("agents:profileInUseIntro")}

@@ -210,9 +210,9 @@ export function AgentProfileDeleteConflictDialog({ /> {hasHardBlockers ? ( -

{t("agents:changeTierMappingsFirst")}

+

{t("agents:changeTierMappingsFirst")}

) : ( -

{t("agents:deleteAnywayConsequences")}

+

{t("agents:deleteAnywayConsequences")}

)}
@@ -242,9 +242,9 @@ function UtilityAgentConflictSection({ const { t } = useTranslation(); if (utilityAgents.length === 0) return null; return ( -
+

{t("agents:conflictUtilityAgentsTitle")}

-
    +
      {utilityAgents.map((agent) => (
    • {agent.name || agent.id} @@ -266,9 +266,9 @@ function SessionConflictSection({ }) { if (sessions.length === 0) return null; return ( -
      +

      {title}

      -
        +
          {sessions.map((t) => (
        • {t.task_title || fallback} @@ -288,9 +288,9 @@ function WatcherConflictSection({ const entries = Object.entries(watchersByKind); if (entries.length === 0) return null; return ( -
          +

          {t("agents:conflictWatchersTitle")}

          -
            +
              {entries.map(([kind, items]) => (
            • @@ -317,9 +317,9 @@ function AutomationConflictSection({ const { t } = useTranslation(); if (automations.length === 0) return null; return ( -
              +

              {t("agents:conflictAutomationsTitle")}

              -
                +
                  {automations.map((ref) => (
                • +

                  {t("agents:conflictTierMappingsTitle")}

                  -
                    +
                      {routingTiers.map((ref) => (