Files
kandev/docs/specs/ui/system-design/confirmation-warning-hierarchy.md
T
2026-09-22 23:31:49 +01:00

18 KiB

status, system, requirements, updated
status system requirements updated
current ui
REQ-TASKS-CONFIRMATION-WARNING-001
REQ-TASKS-CONFIRMATION-SURFACE-002
REQ-UI-TASK-CLEANUP-CONFIRMATION-001
2026-09-10

Task Confirmation Surface System Design

Purpose and boundaries

This design owns the presentation contract for the shared still-working warning, the fine-pointer archive confirmation surface, and the cleanup consequence hierarchy used by task archive and delete workflows. It also owns the explicit discard choice for task worktrees. Task state and cleanup rules remain owned by the task runtime contract.

Requirement mapping

Phone archive surface selection is now owned by the mobile action confirmation design. The popover and inline mechanics below remain the desktop/tablet contract. Cleanup content and full task-delete consent remain owned here.

Requirement Design section
REQ-TASKS-CONFIRMATION-WARNING-001 Components and responsibilities and Mobile and desktop containment
REQ-TASKS-CONFIRMATION-SURFACE-002 Classification-gated surface selection, Popover width contract, Fine-pointer mounting, and Mobile and desktop containment
REQ-UI-TASK-CLEANUP-CONFIRMATION-001 Task cleanup content model, Full-dialog composition, Compact archive surfaces, Source-specific archive routing, and Mobile and desktop containment

Components and responsibilities

  • apps/web/components/task/task-still-working-warning.tsx remains the single markup and style owner.
  • TaskArchiveConfirmDialog and TaskDeleteConfirmDialog render it inside the existing full confirmation dialog.
  • TaskArchiveConfirmation routes the card-owned request to the existing dialog, desktop popover, or explicitly inline branch while reusing the same cleanup content and callbacks.
  • Consumers continue to decide whether a warning mounts. The shared component does not inspect task state or alter callbacks.

Task cleanup content model

getCleanupSummary and getBulkCleanupSummary remain the single place that maps executor types to localized cleanup consequences. Their return value moves from a flat lines array to structured effects and supporting notes. Effects cover resources that are stopped, removed, or destroyed. Notes explain unaffected repository scope or best-effort qualifications. The generic running session effect remains present for every executor path.

The task subject sentence stays outside that model because archive and delete describe different task outcomes. Single-task delete uses direct declarative copy naming the task and its irreversibility. Archive uses corresponding archive copy without describing archive as irreversible. Bulk strings retain locale-aware count handling. Translations land together in the five real catalogs: en, pt-pt, zh-cn, zh-hk, and zh-tw. The Traditional Chinese catalogs are generated through the repository's i18n:zh-hant workflow. The QA-only pseudo locale remains hidden in production, must stay synchronized with en under i18n:check, and is regenerated with pnpm run i18n:pseudo.

A shared task-local renderer presents effects as a semantic list and notes as supporting prose. Full dialogs use paragraphs and a list. The existing compact archive popover and inline confirmation use the same ordered model with their established compact spacing. This removes copy drift without moving cleanup policy into a visual component.

Single-task delete also accepts an inherited-parent workspace context. When that context is true, getCleanupSummary returns only the generic agent-session effect and the localized note that the parent task's worktree, branch, and files remain untouched. It does not return the worktree deletion effect or its single-worktree reassurance. TaskDeleteConfirmDialog receives this context from task-aware callers and, when a caller provides only taskId, resolves workspaceMode from kanbanMulti.snapshots with kanban.tasks as the fallback. If the task cannot be resolved, the existing executor-based summary remains the safe fallback.

Full-dialog composition

TaskDeleteConfirmDialog keeps Radix AlertDialog and the existing centered inset surface, including size="lg" on phones. Archive shares its state and content across the existing non-phone alert and the new phone confirmation surface described in the mobile design.

TaskDeleteConfirmDialog shows an unchecked discard selection only when fresh inspection finds dirty worktrees in the selected scope. Clean results hide the selection. Loading or failed inspection disables Delete without showing discard consent. The label states that tracked and untracked files will be permanently removed. Dirty results require explicit selection before Delete is enabled. The task cleanup design owns inspection, cascade scope, retry, and stale-response handling, including retained worktrees whose task has no executor projection.

Each full surface uses an auto/minmax/auto layout: title in the first row, one minmax(0, 1fr) body containing description, cleanup consequences, warning, and cascade choice, then the persistent action footer. The surface is capped by the dynamic viewport; the body owns vertical overflow. Short confirmations retain intrinsic height, while long task names, bulk executor groups, longer locales, warnings, and cascade copy remain reachable.

Both footer actions use min-h-11 w-full below sm, restoring compact automatic dimensions at sm. Delete selects variant="destructive" on AlertDialogAction and removes manual color utilities. This matters because the action is slotted through Button: a default wrapper currently contributes bg-primary while the child contributes bg-destructive, and stylesheet order allows the primary color to win. Archive continues to use the default action variant.

Compact archive surfaces

TaskArchiveConfirmation consumes the same structured cleanup model for its fine-pointer popover and coarse-pointer inline confirmation. It keeps the existing ActionConfirmPopover and InlineConfirmActions components, widths, touch density, callbacks, and focus-return behavior. Only the internal copy hierarchy changes: direct archive outcome, ordered effects, then supporting notes and the existing still-working warning.

Source-specific archive routing

Phone archive uses a confirmation step in an open owned surface, or a compact standalone bottom drawer from a page. This includes Kanban, task rows, direct archive-flow callers, bulk archive, and the phone command panel's owned step. The mobile branch precedes legacy forceDialog, inline, and pointer routing. Cards and rows do not grow while the confirmation is open.

Fine-pointer desktop and compact-desktop retain the anchored popover; coarse-pointer tablet retains its inline zero-descendant surface. Non-phone command-panel callers retain explicit inline confirmation. The mobile design owns focus transfer, retained host content, and cancellation restoration.

Classification-gated surface selection

TaskArchiveConfirmation keeps descendant classification as the authoritative input for choosing between the compact confirmation and the cascade dialog. A standard fine-pointer caller does not mount a provisional popover while the classification is idle or loading. A resolved zero count mounts the anchored popover; a positive count mounts the full cascade dialog; and an error mounts the same full dialog as the fail-safe path. No archive action is exposed before that choice is known.

The controlled archive request remains open while classification is pending, so the final surface can mount without another user action. This avoids moving focus and assistive-technology context from a temporary popover into a dialog. It also preserves the existing final surfaces and their callbacks instead of adding cascade controls to the compact popover.

A non-rendering pending-dismissal controller retains the lifecycle previously owned by the provisional popover. Escape closes the controlled request and returns focus to the configured trigger; a new pointer interaction closes it without preventing the new target's interaction. If live data removes the originating anchor, the controller closes the request before classification can select a final surface. Closing disables the classification hook, whose cleanup ignores any late response, so a dismissed request cannot surface later. The controller mounts no role, action, or visual confirmation shell.

Callers whose final presentation is already known keep their current behavior. Non-phone forced and bulk operations may mount the full dialog immediately with its action disabled until classification settles. Phone archive may mount its known mobile surface with the same disabled action. Tablet inline confirmations retain their established loading treatment.

Popover width contract

ActionConfirmPopover gains a small width/size contract whose default remains the current w-64 surface. The archive confirmation opts into a modest wider variant, targeting w-72, with a viewport-aware maximum width such as max-w-[calc(100vw-1rem)]. The class contract stays local to the shared popover primitive, so watcher and other confirmation consumers do not widen implicitly. The archive body keeps text-pretty wrapping and the existing title/action hierarchy.

Fine-pointer mounting

At the TaskItemWithContextMenu adapter boundary, useResponsiveBreakpoint determines where the already-created archive confirmation node mounts:

  • Fine-pointer confirmation mounts as a sibling of the cloned task row inside the existing anchor wrapper. It does not pass archiveConfirmation into TaskItem, so TaskItem does not add its flex-wrap row branch or the basis-full action slot for a portaled popover.
  • Tablet coarse-pointer confirmation continues through TaskItem's existing inline action slot. Phone confirmation mounts beside the row and uses the mobile host, leaving the origin row's geometry unchanged.

Both branches reuse the same useTaskSwitcherArchiveConfirmation node, callbacks, anchor ref, and focus-return ref. No business logic or duplicate confirmation markup is introduced. The change removes the layout cause of fine-pointer row growth instead of compensating for it with row dimensions.

Data and contracts

The component preserves data-testid="still-working-warning", role="alert", the translated task:stillWorkingWarning and subject keys, and the existing yellow border/background/text classes. The compact style contract is:

  • warning container: gap-1.5, p-2.5, text-xs, leading-5, and text-pretty;
  • warning icon: h-3.5 w-3.5, mt-0.5, and shrink-0;
  • existing rounded border, yellow semantic colors, and dark-mode contrast stay unchanged.

The dialog passes discardWorktreeChanges with the existing cascade choice. The task API sends it as discard_worktree_changes=true. A typed HTTP 409 uses task_delete_dirty_worktree when consent is absent. A shared client classifier maps this code to localized feedback for single and bulk deletion surfaces.

Localization catalogs change in the five real locales. The Traditional Chinese catalogs use the existing generation command. No browser state is persisted.

forceDialog is an optional internal presentation prop with a default of false. It is passed from the card-owned source to the existing routing component and does not change archive commands, descendant classification, preference state, mutation state, or callbacks.

Control flow

The existing task-level foregroundActivity projection and explicit isInFlight props continue to determine whether the warning is rendered. The dialog computes the localized task outcome and cleanup model during render, then passes the model to the shared task-local renderer. Archive/delete callbacks and domain state remain unchanged. Phone sources choose the shared mobile surface before legacy forceDialog and explicit-inline selection. Outside phone widths, descendant classification settles before a standard fine-pointer request mounts its popover or dialog. Tablet coarse-pointer and explicit non-phone inline callers retain their current routing. The mobile design owns presentation and focus timing; archive commands, preference state, classification and callbacks retain their existing ownership.

The delete dialog resets discard selection when it closes. On confirm, it sends the cascade and discard choices through the existing callback. A typed dirty conflict keeps the task in every local cache and shows one localized message. Other deletion errors keep their existing error handling.

Failure and recovery

A failed descendant classification selects the full dialog without first mounting a compact confirmation. If localized text is longer than the available width, the shared surface-text contract wraps it inside the body. If content is taller than the dynamic viewport, the body scrolls without moving the title or actions.

If deletion returns task_delete_dirty_worktree, the task remains visible. The localized message tells the user to reopen the confirmation and select discard. Unknown executor types retain the generic running session effect. If no task activity is present, no warning mounts, as before.

Persistence

The browser does not persist discard selection or consent. Backend cleanup-snapshot persistence, the discard_worktree_changes contract, and typed HTTP 409 handling are defined in Dirty worktree task deletion.

Observability

Existing component tests continue to cover warning visibility and cleanup content. Cleanup-summary tests cover every executor, bulk grouping, ordering, effects, and supporting notes. Delete-dialog tests cover selection reset, disabled actions, single and bulk callback values, cascade behavior, and typed conflict feedback. Dialog and compact-surface tests assert equivalent structured content, semantic action variants, unchanged callbacks, and that a deferred positive descendant result does not expose a provisional fine-pointer popover. The inherited-parent cleanup path is covered at both the summary and rendered dialog boundaries, including the task-id resolution fallback. Pending-dismissal component cases cover Escape, trigger focus restoration, and outside pointer intent. Rendered desktop and phone checks cover viewport bounds, scroll ownership, action reach, and document overflow. The desktop cascade E2E holds the descendant-count response, dismisses the invisible request, proves a late result cannot mount a confirmation, then reopens Archive and observes only the cascade dialog.

Mobile and desktop containment

The desktop check uses the real sidebar task row with a fine pointer. It records the row getBoundingClientRect().height before opening Archive, while the archive popover is visible, and after Cancel; all three values must remain stable within subpixel precision. It also verifies the archive popover is strictly wider than 256px and remains inside the viewport at compact widths. For a parent task, a delayed descendant-count response leaves the confirmation shell absent until classification settles; after release, only the full cascade dialog is rendered and the anchored popover never becomes visible.

The phone Kanban check enters through a card overflow menu and expects an inset bottom confirmation even for a zero-descendant task. It retains stable card geometry, stacked actions, cleanup copy, theme contrast, cancellation/focus, and zero horizontal-overflow checks.

The phone task-switcher check expects one confirmation step inside the same drawer, unchanged row geometry, and restoration of list state/scroll on Cancel. It asserts reachable actions, one active scroll owner, and no horizontal overflow.

The task-delete phone check enters through the real task drawer action menu and opens Delete with a long task title and a longer bundled locale. After portal animations settle, it verifies the surface's 16px viewport insets, zero document horizontal overflow, title/body wrapping, body scroll ownership when needed, and visible persistent actions. The discard label has a 44px touch target and remains inside the scrolling body. Both footer actions remain full-width and at least 44px high. A desktop check retains compact row actions.