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)
42 KiB
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:
autoFocuson 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-foregroundwith 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>withanimate-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
disabledduring mutation — bothdisabledprop ANDpointer-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:
autoFocuson 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
branchManuallyEditedboolean. - 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://, orgit@ - 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:
autoFocuson 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-destructivebelow 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:
-
Tooltip on destructive buttons: On hover (after 800ms delay), show a tooltip:
"Shift+click to skip confirmation". Use shadcn<Tooltip>withtext-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. -
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 asessionStorageflag. This teaches the shortcut at the exact moment it is relevant. -
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]isLoader2from Lucide withanimate-spin h-4 w-4 mr-2- Button text changes to present participle: "Create" -> "Creating...", "Register" -> "Registering...", "Start" -> "Starting...", "Delete" -> "Deleting..."
- Button is
disabledduring 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-1directly below the offending input - Input border:
border-destructive(red ring) — replaces the defaultring-ringfocus 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
isTouchedper 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-describedbylinked to the input viaid. 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.tsxpackages/web/src/components/ui/dialog.tsxpackages/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:
bg-black/80->bg-black/60onDialogOverlay. 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.- Wrap animation utilities in
motion-safe:prefixes forprefers-reduced-motioncompliance. 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:
- Delayed tooltip (800ms) on destructive buttons.
- 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 (
autoFocusprop) - 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 |