Files
Codewalkers/docs/wireframes/v2/dialogs.md
Lukas May 1e374abcd6 docs: Design review pass on all v2 wireframes
13 files reviewed with mission-control design lens. Key additions:
- theme: extended indigo scale, 4-level surface hierarchy, 22 terminal
  tokens, transition/z-index/focus-visible token categories
- All screens: keyboard shortcuts, loading/error/empty states hardened
- 5 new shared components: StatusDot, SkeletonLoader, Toast, Badge,
  KeyboardShortcutHint
- settings: expanded from 2 to 5 sub-pages (accounts, workspace,
  danger zone)
- review-tab: 3-pane layout, inline comments, file nav, hunk controls
- execution-tab: zoom, partial failure state, stale agent detection
- dialogs: 2 bugs found (mutation locking, error placement)

Total: 4,039 → 9,302 lines (+130% from review pass)
2026-03-02 19:36:26 +09:00

880 lines
42 KiB
Markdown

# Dialogs (v2)
All modal/dialog overlays used throughout the application. v2 adds consistent
loading and error states to all mutation-backed dialogs.
**Surface level**: All dialogs render at Level 2 (`--popover`, 16% lightness in dark mode)
with the `shadow-lg` token (light mode only). See theme.md surface hierarchy.
**Max height**: All dialogs cap at `max-h-[85vh]` with internal scroll on the form/body
region. The header and footer remain fixed. This prevents dialogs from overflowing on
small screens or when content is unexpectedly long (e.g., a ProjectPicker with 20 repos).
### Source: `packages/web/src/components/CreateInitiativeDialog.tsx`, `packages/web/src/components/RegisterProjectDialog.tsx`, `packages/web/src/components/RefineSpawnDialog.tsx`, `packages/web/src/components/TaskDetailModal.tsx`, `packages/web/src/components/editor/DeleteSubpageDialog.tsx`
---
## v1 -> v2 Changes
| Aspect | v1 | v2 |
|--------|----|----|
| Create Initiative: Branch field | Present (optional text input) | REMOVED (auto-generated on first execution dispatch) |
| All dialogs: submit loading | Some had it, inconsistent | All show `[spinner] <verb>ing...` on submit button |
| All dialogs: submit error | Some had it, inconsistent | All show red error text below form, above buttons |
| All dialogs: sizing | One-size `max-w-lg` | Size class per dialog type (sm/md/lg) |
| All dialogs: animation | Default shadcn (fade+zoom) | Documented: scale+fade enter, fade+scale exit |
| All dialogs: focus management | Unspecified | Documented per dialog: initial focus, tab order, Esc |
| All dialogs: backdrop behavior | Unspecified | Close-on-backdrop per type (safe for confirmations, guarded for forms) |
| All dialogs: form validation | Submit-only | Inline validation on blur + submit |
---
## Create Initiative Dialog
**Trigger**: "New Initiative" button on `/initiatives`
**Size**: `max-w-lg` (512px) — needs room for ProjectPicker list. This is deliberately wider
than Register Project (`max-w-md`) because the ProjectPicker checkbox rows need horizontal
space for project name + truncated URL side by side. Do NOT widen further — if the project
list exceeds 6 items, the ProjectPicker scrolls internally (`max-h-[200px] overflow-y-auto`).
**Backdrop**: Click outside closes ONLY if form is pristine. If any field is dirty, show `window.confirm("Discard unsaved changes?")`.
### Focus management
- **Open**: `autoFocus` on Name input
- **Tab order**: Name -> Execution Mode -> Project checkboxes -> Register link -> (Advanced toggle if present) -> Cancel -> Create
- **Escape**: Same behavior as backdrop click (close if pristine, confirm if dirty)
### Default state
```
+----------------------------------------------------------+
| Create Initiative [x] |
| |
| Name * |
| [Auth System Overhaul___________________________] |
| |
| Execution Mode |
| [ Review v ] |
| |
| Projects |
| +----------------------------------------------------+ |
| | [x] backend github.com/org/backend-api | |
| | [ ] frontend github.com/org/frontend-app | |
| | [ ] shared github.com/org/shared-lib | |
| | + Register new project | |
| +----------------------------------------------------+ |
| |
| [ Cancel ] [ Create ] |
+----------------------------------------------------------+
```
Note: Branch field has been removed from the default view. Branches are
auto-generated (`cw/<slugified-name>`) on first execution task dispatch.
See auto-branch system in architecture docs.
### Advanced section (collapsed by default)
Power users can override the auto-generated branch name via a collapsible
"Advanced" section. The toggle sits below the Projects picker.
```
| +----------------------------------------------------+ |
| | [x] backend github.com/org/backend-api | |
| | [ ] frontend github.com/org/frontend-app | |
| | + Register new project | |
| +----------------------------------------------------+ |
| |
| [v] Advanced |
| +----------------------------------------------------+ |
| | Branch Name (auto-generated if blank) | |
| | [cw/auth-system-overhaul_________________________] | |
| +----------------------------------------------------+ |
| |
| [ Cancel ] [ Create ] |
```
- Toggle: `text-sm text-muted-foreground` with chevron icon (ChevronRight rotates to ChevronDown).
Use shadcn `<Collapsible>` + `<CollapsibleTrigger>` + `<CollapsibleContent>` — NOT a custom
disclosure widget. The Collapsible primitive handles aria-expanded and content animation.
- Container: `border rounded-md p-3 bg-muted/30` — visually recessed
- Branch input: pre-filled with `cw/<slugified-name>` as placeholder, editable. The placeholder
updates live as the user types in the Name field (slugify on change).
- Collapsed by default — 99% of users should not touch this
- When collapsed, the section occupies a single line (~28px). The dialog height does NOT
jump when toggled — use `<CollapsibleContent>` with `animate-collapsible-down` /
`animate-collapsible-up` (already defined in shadcn's Collapsible animation presets).
### Submitting state
```
+----------------------------------------------------------+
| Create Initiative [x] |
| |
| Name * |
| [Auth System Overhaul___________________________] |
| |
| Execution Mode |
| [ Review v ] |
| |
| Projects |
| +----------------------------------------------------+ |
| | [x] backend github.com/org/backend-api | |
| | [ ] frontend github.com/org/frontend-app | |
| +----------------------------------------------------+ |
| |
| [ Cancel ] [ [spinner] Creating... ] |
+----------------------------------------------------------+
```
- `[ Create ]` button replaced with `[ [spinner] Creating... ]` — uses `<Loader2 className="animate-spin h-4 w-4 mr-2" />`
- Button is `disabled` during mutation — both `disabled` prop AND `pointer-events-none` (belt and suspenders)
- `[ Cancel ]` remains enabled but closing the dialog is prevented
- `[x]` close button: hidden during mutation (not just disabled — reduces visual noise)
- Backdrop click and Escape are both suppressed during mutation via `onInteractOutside` / `onEscapeKeyDown`
### Error state
```
+----------------------------------------------------------+
| Create Initiative [x] |
| |
| Name * |
| [Auth System Overhaul___________________________] |
| |
| Execution Mode |
| [ Review v ] |
| |
| Projects |
| +----------------------------------------------------+ |
| | [x] backend github.com/org/backend-api | |
| +----------------------------------------------------+ |
| |
| Failed to create initiative. |
| [ Cancel ] [ Create ] |
+----------------------------------------------------------+
```
- Error text: `text-sm text-destructive`, positioned between form fields and footer buttons
- Cleared on next submit attempt
- For field-specific errors (e.g., "Name already exists"), show inline below the offending field
instead of the generic error position. Generic server errors stay above buttons.
### Form validation
| Field | Rule | Fires on | Inline message |
|-------|------|----------|----------------|
| Name | Required, non-empty after trim | blur + submit | "Name is required" |
| Name | Unique across initiatives | submit (server) | "An initiative with this name already exists" |
| Execution Mode | Always valid (has default) | -- | -- |
| Projects | Optional, no validation | -- | -- |
| Branch (Advanced) | Optional, valid slug chars | blur | "Branch name can only contain a-z, 0-9, /, -" |
Inline error styling: `text-xs text-destructive mt-1` below the input. Input border changes to `border-destructive`.
**Fields**:
- Name (required text input, `autoFocus`)
- Execution Mode (select: Review per Phase / YOLO)
- Projects (ProjectPicker: checkbox list + register link)
- Branch (optional, inside Advanced collapsible)
**Source**: `packages/web/src/components/CreateInitiativeDialog.tsx`
---
## Register Project Dialog
**Trigger**: "Register Project" button on `/settings/projects` or "+ Register new project" in ProjectPicker
**Size**: `max-w-md` (448px) — simpler form, fewer fields than Create Initiative
**Backdrop**: Click outside closes only if form is pristine. Dirty form triggers `window.confirm("Discard unsaved changes?")`.
### Focus management
- **Open**: `autoFocus` on Project Name input
- **Tab order**: Project Name -> Repository URL -> Default Branch -> Cancel -> Register
- **Escape**: Same as backdrop click
### Default state
```
+----------------------------------------------------------+
| Register Project [x] |
| |
| Project Name * |
| [backend-api_______________________________________] |
| |
| Repository URL * |
| [https://github.com/org/backend-api________________] |
| |
| Default Branch |
| [main______________________________________________] |
| |
| [ Cancel ] [ Register ] |
+----------------------------------------------------------+
```
### Submitting state
```
| [ Cancel ] [ [spinner] Registering... ] |
```
### Error state
```
| Failed to register project. |
| [ Cancel ] [ Register ] |
```
### Branch auto-detection
After the user enters a valid-looking Repository URL and the input loses focus,
fire a debounced (500ms) request to detect the default branch. This avoids
users guessing wrong (some repos use `master`, `develop`, `trunk`).
```
| Repository URL * |
| [https://github.com/org/backend-api________________] |
| |
| Default Branch |
| [[spinner] Detecting...______________________________] |
```
States:
- **Idle**: Input shows "main" as default value, editable
- **Detecting**: Spinner inside input, field temporarily `readonly`, `text-muted-foreground`.
Submit button is NOT disabled during detection — user can submit with the current branch value.
- **Detected**: Value replaced with detected branch (e.g., "master"), field editable again.
If user has already manually edited the branch field before detection completes, do NOT
overwrite — user intent wins. Track a `branchManuallyEdited` boolean.
- **Detection failed**: Keep current value, show subtle hint: `text-xs text-muted-foreground mt-1`
"Could not auto-detect branch. Using current value."
- **Cancelled**: If the user modifies the URL while a detection request is in flight,
cancel the previous request (`AbortController`) and start a new debounce cycle.
Trigger conditions for detection:
- URL field loses focus AND URL has changed since last detection
- URL matches a basic pattern: starts with `http://`, `https://`, or `git@`
- Do NOT fire on every keystroke — only on blur with 500ms debounce
Implementation note: Detection requires a server-side tRPC procedure (e.g., `detectDefaultBranch`)
that runs `git ls-remote --symref <url> HEAD`. This is a nice-to-have; ship without it first,
add later. The wireframe accounts for both states. The procedure MUST have a 5-second timeout —
private repos with bad auth will hang `git ls-remote` indefinitely.
### Form validation
| Field | Rule | Fires on | Inline message |
|-------|------|----------|----------------|
| Project Name | Required, non-empty after trim | blur + submit | "Project name is required" |
| Project Name | Unique | submit (server) | "A project with this name already exists" |
| Repository URL | Required, non-empty | blur + submit | "Repository URL is required" |
| Repository URL | Valid URL format | blur | "Enter a valid git repository URL" |
| Repository URL | Unique | submit (server) | "This repository is already registered" |
| Default Branch | Non-empty (has default "main") | blur | "Branch name is required" |
**Fields**:
- Project Name (required, unique)
- Repository URL (required, unique, git repo URL)
- Default Branch (text input, defaults to "main", auto-detected when possible)
**Source**: `packages/web/src/components/RegisterProjectDialog.tsx`
---
## Refine Spawn Dialog
**Trigger**: "Refine with Agent" button in Content tab, or "Retry" button after agent crash
**Size**: `max-w-md` (448px) — single textarea, compact dialog
**Backdrop**: Click outside always closes (no data loss risk — instructions are optional, low-cost to retype)
### Focus management
- **Open**: `autoFocus` on Instructions textarea
- **Tab order**: Instructions textarea -> Cancel -> Start
- **Escape**: Closes dialog immediately
### Default state
```
+----------------------------------------------------------+
| Refine Content [x] |
| |
| Instructions (optional) |
| +----------------------------------------------------+ |
| | Focus on the authentication flow section. | |
| | Add more detail about the OAuth providers we | |
| | need to support. | |
| | | |
| +----------------------------------------------------+ |
| |
| [ Cancel ] [ Start ] |
+----------------------------------------------------------+
```
### Submitting state
```
| [ Cancel ] [ [spinner] Starting... ] |
```
### Error state
```
| Failed to start agent. |
| [ Cancel ] [ Start ] |
```
**IMPLEMENTATION BUG**: The current `RefineSpawnDialog.tsx` renders `{error && ...}` AFTER
`<DialogFooter>`, placing the error text below the buttons where it is easy to miss. Fix:
move the error rendering ABOVE `<DialogFooter>` to match the pattern used by all other dialogs.
See lines 116-120 of the current implementation.
**Fields**:
- Instructions (optional textarea, free-form guidance for the agent, 3 rows default,
auto-expands up to 8 rows via `min-h-[80px] max-h-[200px] resize-y`)
**Source**: `packages/web/src/components/RefineSpawnDialog.tsx`
---
## Task Detail Modal
**Trigger**: Click any task row in Plan tab or Execution tab
**Size**: `max-w-xl` (576px) — wider to accommodate metadata grid + dependency lists without cramping
**Backdrop**: Click outside closes IF no inline edit is active. If the description textarea is
open with unsaved changes, show `window.confirm("Discard description changes?")`. If the
textarea is open but content hasn't changed, close immediately.
### Focus management
- **Open**: Focus the dialog container itself (metadata is read-only, description edit is opt-in)
- **Tab order**: Description area (if pending task) -> Queue Task -> Stop Task -> Close (X) button
- **Escape**: Closes dialog if no inline edit is active. If description textarea is open,
first Escape cancels the edit (reverts text), second Escape closes the dialog.
### Default state
```
+----------------------------------------------------------+
| Implement PKCE Flow [x] |
| |
| +------------------------+-------------------------+ |
| | Status [IN_PROGRESS]| Priority normal | |
| | Phase 2. OAuth Flow| Type execute | |
| | Agent blue-fox-7 | |
| +------------------------+-------------------------+ |
| |
| Description |
| Implement the OAuth 2.0 PKCE extension for public |
| clients. Generate code verifier/challenge pair, |
| include in authorization request, and validate |
| during token exchange. |
| |
| Dependencies |
| * Set up OAuth routes [DONE] |
| |
| Blocks |
| * GitHub provider adapter [BLOCKED] |
| * Microsoft provider adapter [PENDING] |
| |
| [ Queue Task ] [ Stop Task ] |
+----------------------------------------------------------+
```
### Queue in progress
```
| [ [spinner] Queuing... ] [ Stop Task ] |
```
### Stop in progress
```
| [ Queue Task ] [ [spinner] Stopping... ] |
```
### Error state
```
| Failed to queue task. Try again. |
| [ Queue Task ] [ Stop Task ] |
```
**Fields** (read-only with inline edit affordances):
- 2-column metadata grid: Status (with badge), Priority, Phase, Type, Agent
- Description text — **click to edit** (pencil icon on hover, expands to textarea, saves on blur/Enter)
- Dependencies list (with StatusDot per dependency)
- Blocks list (dependents with StatusDot)
### Inline description editing
The Description section shows a subtle edit affordance on hover:
```
| Description [pencil] |
| Implement the OAuth 2.0 PKCE extension for public |
| clients. Generate code verifier/challenge pair... |
```
On click, the text block becomes an auto-sizing `<Textarea>`:
```
| Description [save] |
| +----------------------------------------------------+ |
| | Implement the OAuth 2.0 PKCE extension for public | |
| | clients. Generate code verifier/challenge pair, | |
| | include in authorization request, and validate | |
| | during token exchange. | |
| +----------------------------------------------------+ |
```
- Save triggers on blur or Cmd+Enter (Mac) / Ctrl+Enter (Windows)
- Cancel on Escape (reverts to original text, closes textarea back to static view)
- Mutation: `updateTask({ id, description })` — optimistic update on the local task cache
- **Saving state**: The `[save]` button shows `[spinner]` during mutation. Textarea remains
editable but a second save is debounced until the first completes.
- **Save error**: Inline `text-xs text-destructive` below the textarea: "Failed to save. Try again."
The textarea stays open so the user can retry without losing their edit.
- Only enabled when task status is `pending` — in-progress/completed tasks lock description.
The pencil icon is hidden entirely (not grayed out) for non-pending tasks to avoid
implying an action that is not available.
- **Empty state**: If description is null/empty and task is pending, show
"Click to add description" as placeholder text in `text-muted-foreground italic`.
### Agent field
Agent row shows the assigned agent name. For pending tasks with no agent assigned,
this is not editable — agent assignment happens through the dispatch system.
Showing "Unassigned" in `text-muted-foreground` with no edit control is correct.
For in-progress tasks, the agent name should be a clickable link that navigates to
the agent's output view (same as clicking the agent name in the agents panel).
Use `text-primary hover:underline cursor-pointer` styling.
**Actions**:
- `[ Queue Task ]` -- `variant="outline" size="sm"`, disabled if already running/queued or dependencies incomplete
- `[ Stop Task ]` -- `variant="destructive" size="sm"`, disabled if not running
**Source**: `packages/web/src/components/TaskDetailModal.tsx`
---
## Delete Subpage Dialog
**Trigger**: Auto-triggered when a page link is deleted in the Tiptap editor
**Size**: `max-w-sm` (384px) — confirmation only, minimal content, no form fields
**Backdrop**: Click outside closes and keeps the subpage (same as clicking "Keep Subpage")
### Focus management
- **Open**: Focus the "Keep Subpage" button (safe default — prevents accidental deletion via Enter)
- **Tab order**: Keep Subpage -> Delete
- **Escape**: Closes dialog, keeps subpage
### Default state
```
+----------------------------------------------------------+
| Delete Subpage? [x] |
| |
| You removed a link to "Token Rotation". Do you |
| want to delete the subpage as well? |
| |
| [ Keep Subpage ] [ Delete ] |
+----------------------------------------------------------+
```
### Delete in progress
```
| [ Keep Subpage ] [ [spinner] Deleting... ] |
```
### Error state
```
| Failed to delete subpage. |
| [ Keep Subpage ] [ Delete ] |
```
**Fields**: None (confirmation dialog only)
**Cascade warning**: If the subpage has its own child subpages, the confirmation message
should include the count: "This will also delete 3 child pages." The message adapts:
```
| You removed a link to "Token Rotation". Do you |
| want to delete the subpage and its 3 child pages? |
```
This prevents users from accidentally nuking an entire page subtree. The child page count
comes from the `pages` table (count where `parentPageId = targetPageId`).
**Actions**:
- `[ Keep Subpage ]` -- `variant="outline"`, closes dialog without deleting. This is the
focused button on open (safe default).
- `[ Delete ]` -- `variant="destructive"`, deletes the subpage and its content (cascade via FK)
**Source**: `packages/web/src/components/editor/DeleteSubpageDialog.tsx`
---
## Confirmation Dialogs (window.confirm)
Used for destructive actions throughout the app. All support **Shift+click to bypass**.
| Action | Trigger Location | Message |
|--------|-----------------|---------|
| Delete initiative | Initiative card `[...]` menu | "Delete this initiative?" |
| Archive initiative | Initiative card `[...]` menu | "Archive this initiative?" |
| Delete phase | Phase detail `[...]` menu | "Delete this phase?" |
| Delete task | Task row `[x]` button | "Delete this task?" |
| Delete project | Project card `[trash]` button | "Delete this project?" |
| Delete agent | Agent actions `[...]` menu | "Delete this agent?" |
| Stop agent (from inbox) | Inbox detail `[ Stop Agent ]` button | "Stop this agent? It will not be able to resume." |
Note: All `window.confirm()` dialogs are synchronous browser dialogs. They do not
receive the v2 loading/error treatment since the mutation fires only after confirmation.
### Shift+click discoverability
The Shift+click bypass is power-user-friendly but completely undiscoverable. Three mitigations:
1. **Tooltip on destructive buttons**: On hover (after 800ms delay), show a tooltip:
`"Shift+click to skip confirmation"`. Use shadcn `<Tooltip>` with `text-xs`. The 800ms
delay is critical — a shorter delay would fire on every pass-through hover and become
noise. Only users who pause on the button see it, which is exactly the audience that
benefits from the hint.
2. **First-time hint in confirmation dialog**: The FIRST time a user sees a `window.confirm()`
dialog in a session, append to the message: `\n\n(Tip: Hold Shift and click to skip this dialog)`
After the first occurrence, suppress the hint for the rest of the session via a
`sessionStorage` flag. This teaches the shortcut at the exact moment it is relevant.
3. **NOT a "Don't ask again" checkbox**. Persistent suppression of confirmations is dangerous
in a multi-agent system where a misclick can delete an initiative with 50 tasks and 10
running agents. Shift+click is intentional per-action, which is the right granularity.
A checkbox would be a footgun. The session-scoped hint in (2) is the compromise —
teach the shortcut without offering permanent suppression.
---
## Shared Patterns Across All Dialogs
### Dialog sizing classes
Dialogs are NOT one-size-fits-all. Use the narrowest class that fits the content:
| Size | Tailwind | Pixels | Used by |
|------|----------|--------|---------|
| `sm` | `max-w-sm` | 384px | Delete Subpage, all confirmation-style dialogs |
| `md` | `max-w-md` | 448px | Register Project, Refine Spawn |
| `lg` | `max-w-lg` | 512px | Create Initiative (default shadcn, used when ProjectPicker needs room) |
| `xl` | `max-w-xl` | 576px | Task Detail Modal (metadata grid + lists need breathing room) |
Override the default `max-w-lg` on `<DialogContent>` by passing `className="max-w-sm"` etc.
### Dialog animation
All dialogs use the shadcn/Radix animation classes already present in `dialog.tsx`:
- **Enter**: `fade-in-0` + `zoom-in-95` + `slide-in-from-top-[48%]` — 200ms duration
- **Exit**: `fade-out-0` + `zoom-out-95` + `slide-out-to-top-[48%]` — 200ms duration
This is already implemented in the codebase (`data-[state=open/closed]` animations).
The spec documents it here for design intent clarity: the slight zoom (95% -> 100%)
combined with the vertical slide gives a "materializing" feel appropriate for mission
control. No changes needed to `dialog.tsx`.
**Backdrop**: `fade-in-0` / `fade-out-0` on the overlay. Already implemented. The
`bg-black/80` opacity is heavy — reduce to `bg-black/60` for dark mode where the
contrast is already high. Implementation: replace `bg-black/80` in `DialogOverlay`
with `bg-black/60 dark:bg-black/60` (or just `bg-black/60` since the app is
dark-mode-first). The lighter overlay keeps the "mission control behind glass" feel
without the claustrophobic blackout.
**Reduced motion**: Wrap all animation classes in `motion-safe:` prefix. Users with
`prefers-reduced-motion: reduce` get instant transitions. Already supported by Tailwind —
just prefix the animation utilities. The shadcn default `dialog.tsx` does NOT do this;
it needs to be patched.
### Submit button loading state
```
v1: [ Create ]
v2: [ [spinner] Creating... ]
```
- `[spinner]` is `Loader2` from Lucide with `animate-spin h-4 w-4 mr-2`
- Button text changes to present participle: "Create" -> "Creating...", "Register" -> "Registering...", "Start" -> "Starting...", "Delete" -> "Deleting..."
- Button is `disabled` during mutation
### Error text placement
Two tiers of error display:
**1. Field-level inline errors** (validation failures for specific fields):
```
+----------------------------------------------------------+
| Name * |
| [___________________________________________________] |
| Name is required <- text-xs, red |
| |
| ...remaining fields... |
+----------------------------------------------------------+
```
- Styling: `text-xs text-destructive mt-1` directly below the offending input
- Input border: `border-destructive` (red ring) — replaces the default `ring-ring` focus style
- Fires on blur for format validation, on submit for all rules
- Does NOT fire on blur if the field has never been touched (prevents errors flashing when
tabbing through an empty form). Track `isTouched` per field — only validate on blur after
the first interaction (focus + blur cycle).
- Clears when user starts typing in the field (optimistic clear — re-validates on next blur)
- **Aria**: Error messages use `aria-describedby` linked to the input via `id`. Screen readers
announce the error when the input receives focus.
**2. Generic server errors** (network failures, unexpected errors):
```
+----------------------------------------------------------+
| ...form fields... |
| |
| <error message text> |
| [ Cancel ] [ Submit ] |
+----------------------------------------------------------+
```
- Styling: `text-sm text-destructive text-center`
- Positioned between form content and footer buttons
- Cleared automatically when the user initiates a new submit
- This position is correct for generic errors — it sits at the "decision point"
where the user is about to click Submit, so it is highly visible
Note: the implementation currently places error text correctly (above footer) in
`CreateInitiativeDialog.tsx` and `RegisterProjectDialog.tsx`, but `RefineSpawnDialog.tsx`
renders it BELOW the footer (`DialogFooter` then `{error && ...}`). This is a bug
in the implementation — fix it to match this spec.
---
## Source (shared)
- `packages/web/src/components/ProjectPicker.tsx`
- `packages/web/src/components/ui/dialog.tsx`
- `packages/web/src/components/ui/button.tsx`
---
## Design Review Notes
Reviewed 2026-03-02. This section captures design decisions, rejected alternatives,
implementation gaps, and outstanding questions from the design review.
### 1. Create Initiative: Advanced section for branch override
**Decision**: Added collapsible "Advanced" section containing the branch name field.
Collapsed by default. 99% of users will never touch it — the auto-generated
`cw/<slug>` branch is the right default. But power users managing multiple initiatives
on the same repo need a way to control branch naming.
**Rejected alternative**: Putting branch back as a top-level optional field. This
was removed for good reason (v1 -> v2 simplification). Hiding it behind a collapse
preserves the simplification while maintaining the escape hatch.
**Implementation note**: The current `CreateInitiativeDialog.tsx` (lines 114-127) still
has a top-level `branch` field from v1. It needs to be wrapped in shadcn's `<Collapsible>`
component. The state already exists (`branch` / `setBranch`) — only the template needs
restructuring. The placeholder should be dynamically derived from the name field via slugify.
### 2. Dialog sizing is not one-size-fits-all
**Problem found**: All five dialog components pass no `className` override to
`<DialogContent>`, so they all render at the default `max-w-lg` (512px). Confirmed by
reading the implementations. A "Delete Subpage?" confirmation with two sentences of text
does not need the same width as Create Initiative with its ProjectPicker list.
**Decision**: Introduced a 4-tier sizing system (sm/md/lg/xl). Each dialog gets the
narrowest size that fits its content. Wider dialogs feel empty and unfocused. Narrow
dialogs feel purposeful. This is purely a className override — zero breaking changes.
**Vertical overflow**: Added `max-h-[85vh]` constraint with internal scroll on the body
region. The Create Initiative dialog is the primary beneficiary — a workspace with 15+
registered projects would push the ProjectPicker below the fold without this.
### 3. Animation: already implemented, needs reduced-motion and backdrop fix
**Finding**: The codebase already has correct dialog animations via shadcn's
`data-[state=open/closed]` classes on `DialogContent` and `DialogOverlay` in
`dialog.tsx`. The spec just never documented it. Now it does.
**Action required**: Two changes to `dialog.tsx`:
1. `bg-black/80` -> `bg-black/60` on `DialogOverlay`. The 80% opacity over a dark
(`#111114`) background creates an almost opaque overlay. 60% is enough to signal
"modal context" without feeling like a blackout curtain.
2. Wrap animation utilities in `motion-safe:` prefixes for `prefers-reduced-motion`
compliance. This is a one-line-per-class change. Tailwind handles it natively.
### 4. Register Project: branch auto-detection is a nice-to-have with sharp edges
**Decision**: Spec now includes a "Detecting..." state for the Default Branch field
after URL input. This prevents users from leaving "main" as default when their repo
actually uses "master" or "develop" or "trunk".
**Implementation cost**: Requires a new tRPC procedure (`detectDefaultBranch`) that
shells out to `git ls-remote --symref <url> HEAD`. This is low-risk (read-only git
operation) but adds a server round-trip. Ship the dialog without detection first,
add it in a follow-up. The wireframe accounts for both paths.
**Edge cases documented inline**:
- User edits branch manually before detection completes -> detection result is discarded
- User modifies URL while detection is in-flight -> previous request is aborted
- Private repos with bad auth -> 5-second timeout, graceful fallback message
- Submit button is NOT blocked by pending detection (user can always proceed with current value)
### 5. Task Detail Modal: surgically editable, not fully read-only
**Decision**: Description field gains inline editing (click-to-textarea with pencil
affordance) for pending tasks only. In-progress and completed tasks remain locked.
The pencil icon is hidden entirely for non-pending tasks (not grayed out — a disabled
edit icon implies the feature exists but is locked, which creates a "why can't I?"
question with no good answer).
**Rejected alternative**: Full inline editing of all fields (reassign agent,
change priority, etc.). Agent assignment happens through the dispatch system —
letting users manually reassign would create conflicts with the dispatch queue.
Priority changes on in-progress tasks would be meaningless since the agent is
already working. Keep it simple: only description is editable, only on pending tasks.
**Added**: Agent name as clickable link for in-progress tasks — navigates to agent output
view. This transforms a static label into a useful navigation shortcut for the most common
follow-up action after checking task status ("let me see what the agent is doing").
**Backdrop behavior updated**: The Task Detail Modal is no longer purely "always closes on
backdrop click." If the description textarea is open with unsaved changes, a confirmation
dialog fires. This is a natural consequence of adding inline editing — the dialog is no
longer fully read-only.
### 6. Backdrop behavior: form-aware, not blanket
**Decision**: Four behaviors based on dialog state:
| Dialog type | Backdrop click | Rationale |
|-------------|---------------|-----------|
| Confirmation (Delete Subpage) | Closes, takes safe action | No data to lose |
| Read-only (Task Detail, no edit active) | Closes | No data to lose |
| Task Detail with description edit active | Confirms if dirty | Inline edit has unsaved text |
| Form with optional fields only (Refine Spawn) | Closes | Instructions are low-cost to retype |
| Form with required fields (Create Initiative, Register Project) | Closes if pristine, confirms if dirty | Prevents accidental data loss |
Radix `Dialog` supports `onInteractOutside` for this. Track form dirtiness via a
simple `isDirty` boolean (any field differs from initial value). For Task Detail,
track `isEditingDescription` and `descriptionIsDirty` separately.
### 7. Shift+click: discoverable via tooltip + first-use hint
**Decision**: Two complementary discoverability mechanisms:
1. Delayed tooltip (800ms) on destructive buttons.
2. First-time hint appended to the `window.confirm()` message, scoped to the session.
The tooltip teaches via observation (hover). The in-dialog hint teaches via action
(the exact moment the user hits the confirmation wall). Together, they cover both
the "browsing" and "acting" learning moments.
**Rejected alternative**: "Don't ask again" checkbox. In a multi-agent system
where one misclick can delete an initiative with 50 tasks and 10 running agents,
permanent suppression of confirmations is reckless. Shift+click is per-action
intentionality, which is the correct granularity.
### 8. Focus management: now documented per dialog
**Decision**: Each dialog section now specifies initial focus target, tab order,
and Escape behavior. Key principles:
- **Form dialogs**: Focus first input field (`autoFocus` prop)
- **Confirmation dialogs**: Focus the SAFE action (Keep Subpage, Cancel) to prevent
accidental destructive actions via Enter key
- **Read-only dialogs**: Focus the dialog container itself
- **Escape**: Context-aware — closes immediately for clean state, confirms for dirty state
Radix Dialog already traps focus and returns it to the trigger on close. The main
implementation gap is that no dialog currently sets `autoFocus` on the safe action
for confirmation dialogs — they all rely on Radix's default (first focusable element),
which happens to be the close `[x]` button. This needs explicit `autoFocus` on the
"Keep Subpage" button in `DeleteSubpageDialog`.
### 9. Error positioning: two-tier system with an existing bug
**Decision**: Field-specific validation errors render inline below the field.
Generic server errors render between form content and footer buttons. This gives
users the right level of locality — field errors point at the problem, server
errors sit at the decision point.
**Bug confirmed**: `RefineSpawnDialog.tsx` renders `{error && ...}` AFTER `<DialogFooter>`
(line 116), placing the error text below the buttons. Verified by reading the source.
This contradicts every other dialog in the system and makes the error easy to miss.
The fix is a 3-line move (lines 116-120 to before line 101).
**Aria note**: Error messages need `role="alert"` so screen readers announce them
when they appear. None of the current implementations do this.
### 10. Form validation: blur + submit, with isTouched guard
**Decision**: Validation fires on blur for format-checkable rules (required, URL format,
slug characters) and on submit for server-validated rules (uniqueness). This gives
immediate feedback without being annoying.
**Critical nuance added**: Validation on blur ONLY fires after the field has been touched
(focused then blurred at least once). Without this guard, tabbing through an empty form
flashes "required" errors on every field — hostile UX that punishes exploration.
**What we do NOT do**: Real-time validation on every keystroke. Showing "Name is
required" while the user is still typing their first character is hostile UX.
Blur-based validation respects the user's intent to finish typing before judging.
**Accessibility**: All inline error messages use `aria-describedby` linked to their
input. This is not optional — it is a WCAG 2.1 Level A requirement.
### 11. Delete Subpage: cascade awareness
**Added during review**: The Delete Subpage dialog now includes a cascade count when
the target page has child subpages. "Do you want to delete the subpage and its 3 child
pages?" This prevents users from accidentally deleting an entire page subtree with one
click. The `pages` table already has `parentPageId` with cascade delete — the data is
there, the UI just needs to surface the consequence.
### 12. Mutation state: preventing double-submit and mid-flight interactions
**Added during review**: All submitting states now explicitly suppress backdrop click,
Escape, and the `[x]` close button. The current implementations only disable the submit
button — but the user can still close the dialog via Escape or backdrop click while a
mutation is in flight, causing the mutation to complete with no visible feedback (the
dialog is gone). This is a real bug in all five dialog components.
Implementation: During `isPending`, set `onInteractOutside={(e) => e.preventDefault()}`
and `onEscapeKeyDown={(e) => e.preventDefault()}` on `<DialogContent>`. Hide (not disable)
the `[x]` button via conditional rendering.
### Implementation gap summary
| # | Gap | File(s) | Priority |
|---|-----|---------|----------|
| 1 | Branch field needs to move into `<Collapsible>` | `CreateInitiativeDialog.tsx` | Medium |
| 2 | Dialog sizes need per-dialog `className` overrides | All 5 dialog components | Low |
| 3 | Error text positioned after footer | `RefineSpawnDialog.tsx` | **High (bug)** |
| 4 | Dialog not locked during mutation (Esc/backdrop still close) | All 5 dialog components | **High (bug)** |
| 5 | Backdrop opacity too heavy in dark mode (`bg-black/80`) | `dialog.tsx` | Low |
| 6 | `motion-safe:` prefixes missing on animation classes | `dialog.tsx` | Low |
| 7 | Inline validation not implemented (blur + isTouched) | All form dialogs | Medium |
| 8 | `aria-describedby` missing on all form error messages | All form dialogs | Medium (a11y) |
| 9 | `role="alert"` missing on server error messages | All form dialogs | Medium (a11y) |
| 10 | Description inline editing not implemented | `TaskDetailModal.tsx` | Medium |
| 11 | Agent name not a clickable link | `TaskDetailModal.tsx` | Low |
| 12 | Delete Subpage cascade count not shown | `DeleteSubpageDialog.tsx` | Medium |
| 13 | Branch auto-detection not implemented | New tRPC procedure + `RegisterProjectDialog.tsx` | Low (nice-to-have) |
| 14 | Shift+click tooltip not implemented | All destructive buttons | Low |
| 15 | First-use confirmation hint not implemented | Shared confirm helper | Low |
| 16 | `autoFocus` on safe button in confirmation dialogs | `DeleteSubpageDialog.tsx` | Low |
| 17 | `max-h-[85vh]` + internal scroll not implemented | All dialog components | Low |
| 18 | ProjectPicker needs `max-h-[200px] overflow-y-auto` | `ProjectPicker.tsx` | Medium |