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)
880 lines
42 KiB
Markdown
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 |
|