Files
Codewalkers/docs/wireframes/v2/shared-components.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

47 KiB

Shared Components (v2)

NEW — no v1 equivalent

Reusable component specifications used across multiple pages. Each component includes: description, props/API, ASCII mockups of all states, usage locations, and proposed source file path.


1. <SaveIndicator>

Inline status indicator for auto-save operations. Shows current save state with icon + label, auto-hides on success.

Props

interface SaveIndicatorProps {
  status: 'idle' | 'saving' | 'saved' | 'error';
  onRetry?: () => void;
  /**
   * When true, "saved" state displays a persistent timestamp ("Saved at 3:42 PM")
   * instead of fading out after 2s. Use for high-stakes contexts like page content
   * editing where the user needs ongoing confirmation that their work is safe.
   * Default: false (fade-out behavior).
   */
  persistent?: boolean;
}

States

Idle:     (hidden — renders nothing)

Saving:   [spinner] Saving...

Saved:    [v] Saved              <- fades out after 2s, transitions to idle

Error:    [!] Failed to save [retry]

Detailed ASCII

Saving

+---------------------------+
|  [spinner] Saving...      |
+---------------------------+
  • text-muted-foreground text-sm
  • Spinner is a 14px animated SVG

Saved (default: persistent=false)

+---------------------------+
|  [v] Saved                |
+---------------------------+
  • text-green-600 text-sm
  • Checkmark icon, 14px
  • Fades to opacity-0 over 300ms after 2s delay, then sets status to idle

Saved (persistent mode: persistent=true)

+---------------------------+
|  [v] Saved at 3:42 PM    |
+---------------------------+
  • Same styling as default saved state, but does NOT fade out
  • Timestamp updates on each save, formatted via Intl.DateTimeFormat with hour/minute
  • Remains visible until next saving or error transition

Error

+--------------------------------------+
|  [!] Failed to save  [retry]        |
+--------------------------------------+
  • text-destructive text-sm
  • Alert icon, 14px
  • [retry] is a text button: text-destructive underline cursor-pointer
  • Does not auto-hide — persists until retry succeeds or user navigates away

Usage

Page Location Persistent?
Content tab Top-right of Tiptap editor toolbar (see content-tab.md) Yes — page content is high-stakes
Plan tab Top-right of phase description editor (see plan-tab.md) No — phase descriptions are lightweight

Source

  • packages/web/src/components/SaveIndicator.tsx (proposed)

2. <EmptyState>

Centered placeholder shown when a list or section has no items. Configurable icon, messaging, and optional CTA button.

Props

interface EmptyStateProps {
  icon: React.ReactNode;       // e.g., <Inbox />, <ListTodo />, <Users />
  title: string;               // e.g., "No initiatives yet"
  description?: string;        // e.g., "Create an initiative to get started."
  action?: {
    label: string;             // e.g., "Plan with Agent"
    onClick: () => void;
    icon?: React.ReactNode;    // e.g., <Sparkles /> — rendered before label
  };
  secondaryAction?: {
    label: string;             // e.g., "Add Phase"
    onClick: () => void;
    icon?: React.ReactNode;    // e.g., <Plus />
  };
}

When both action and secondaryAction are provided, the primary action renders as <Button variant="default"> and the secondary as <Button variant="outline">, side by side with gap-3.

Default (with action)

+-------------------------------------------+
|                                           |
|              [icon]                       |
|         No phases yet                     |
|   Add a phase to start planning.          |
|                                           |
|          [ Add Phase ]                    |
|                                           |
+-------------------------------------------+

Without action

+-------------------------------------------+
|                                           |
|              [icon]                       |
|       No conversations yet                |
|   Questions from agents appear here.      |
|                                           |
+-------------------------------------------+

With primary + secondary action

+-------------------------------------------+
|                                           |
|              [icon]                       |
|         No phases yet                     |
|   Add a phase to start planning,          |
|   or let an agent plan for you.           |
|                                           |
|   [sparkles Plan with Agent]  [+ Add Phase]|
|                                           |
+-------------------------------------------+
  • Primary: <Button variant="default"> (filled indigo)
  • Secondary: <Button variant="outline"> (border only)
  • Container: flex gap-3 justify-center

Without description

+-------------------------------------------+
|                                           |
|              [icon]                       |
|         No agents running                 |
|                                           |
+-------------------------------------------+

Styling

  • Container: flex flex-col items-center justify-center py-16 text-center
  • Icon: h-12 w-12 text-muted-foreground mb-4
  • Title: text-lg font-medium text-foreground mb-1
  • Description: text-sm text-muted-foreground mb-6
  • Action button: default shadcn <Button> variant

Usage

Page Title Primary Action Secondary Action
Initiatives list (no data) "No initiatives yet" "New Initiative" --
Initiatives list (no match) "No matching initiatives" -- (text link instead) --
Plan tab (no phases) "No phases yet" "Plan with Agent" "Add Phase"
Execution tab (no tasks) "No tasks to execute" "Go to Plan Tab" --
Agents page (no agents) "No agents running" -- --
Inbox page (no conversations) "No conversations yet" -- --
Settings > Accounts (none) "No accounts registered" "Add Account" --

Source

  • packages/web/src/components/EmptyState.tsx (proposed)

3. <ErrorState>

Centered error display with optional retry action. Used when a data fetch fails or a section encounters an unrecoverable error.

Props

interface ErrorStateProps {
  message: string;             // e.g., "Failed to load initiatives"
  onRetry?: () => void;        // If provided, shows Retry button
  details?: string;            // Technical error detail (stack trace, API response)
  errorCode?: string;          // e.g., "TRPC_TIMEOUT", "NETWORK_ERROR"
}

With retry

+-------------------------------------------+
|                                           |
|           [AlertCircle]                   |
|      Failed to load initiatives           |
|                                           |
|            [ Retry ]                      |
|                                           |
+-------------------------------------------+

Without retry (navigation fallback)

+-------------------------------------------+
|                                           |
|           [AlertCircle]                   |
|      Error loading initiative             |
|   Could not fetch initiative data.        |
|                                           |
|       [ Back to Initiatives ]             |
|                                           |
+-------------------------------------------+

With details + error code (expandable)

+-------------------------------------------+
|                                           |
|           [AlertCircle]                   |
|      Failed to load initiatives           |
|      Error: TRPC_TIMEOUT                  |
|                                           |
|            [ Retry ]                      |
|                                           |
|      [v] Show details                     |
|      +-----------------------------------+|
|      | TRPCClientError: Request timed    ||
|      | out after 10000ms                 ||
|      |   at fetchQuery (trpc-client.ts:  ||
|      |   42:11)                          ||
|      +-----------------------------------+|
|                                           |
+-------------------------------------------+
  • Error: <code>text-sm text-muted-foreground font-mono, shown below the message when errorCode is provided
  • [v] Show details — collapsible trigger, text-xs text-muted-foreground cursor-pointer
  • Details panel: bg-muted rounded-md p-3 font-mono text-xs text-muted-foreground max-h-32 overflow-y-auto
  • Details are collapsed by default — click to expand. Uses a <details>/<summary> element or state toggle.
  • When details prop is absent, the collapsible section is not rendered at all.

Note: The "without retry" variant is not a prop configuration of <ErrorState> itself. Pages that need navigation instead of retry render <ErrorState> without onRetry and add their own navigation button below it.

Styling

  • Container: flex flex-col items-center justify-center py-16 text-center
  • Icon: h-12 w-12 text-destructive mb-4 (AlertCircle from lucide-react)
  • Message: text-lg font-medium text-foreground mb-4
  • Retry button: <Button variant="outline">Retry</Button>

Usage

Page Message Has Retry?
Initiatives list "Failed to load initiatives" Yes
Initiative detail "Error loading initiative" No (nav button)
Review tab "Failed to load proposals" Yes
Agents page "Failed to load agents" Yes
Settings page "Failed to load settings" Yes

Source

  • packages/web/src/components/ErrorState.tsx (proposed)

4. <CommandPalette>

Global search overlay triggered by keyboard shortcut. Provides quick navigation to initiatives, agents, tasks, and settings pages.

Trigger

  • Cmd+K (Mac) / Ctrl+K (Windows)
  • Header button [cmd-k] (see app-layout.md)

Props

interface CommandPaletteProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
}

Internally fetches data via tRPC queries (initiatives, agents, tasks).

Default State (open, empty query)

+----------------------------------------------------------+
|  [search] Search everything...                     [Esc] |
|  --------------------------------------------------------|
|  Recent                                                  |
|    -> Auth System Overhaul                               |
|    -> Agents                                             |
|    -> Settings                                           |
+----------------------------------------------------------+

Shows recent navigation history when search is empty.

With Search Results

+----------------------------------------------------------+
|  [search] auth___                                  [Esc] |
|  --------------------------------------------------------|
|  Initiatives                                             |
|    Auth System Overhaul                         [ACTIVE] |
|  --------------------------------------------------------|
|  Tasks                                                   |
|    Implement auth middleware             [execute] 3/7   |
|    Review auth token flow               [verify]  done  |
|  --------------------------------------------------------|
|  Agents                                                  |
|    blue-fox-7 (auth middleware)               [RUNNING]  |
+----------------------------------------------------------+

No Results

+----------------------------------------------------------+
|  [search] xyznonexistent___                        [Esc] |
|  --------------------------------------------------------|
|                                                          |
|              No results for "xyznonexistent"             |
|                                                          |
|          Try a shorter query, or browse:                 |
|          -> All Initiatives                              |
|          -> All Agents                                   |
|          -> Settings                                     |
|                                                          |
+----------------------------------------------------------+
  • "Try a shorter query, or browse:" — text-sm text-muted-foreground
  • Fallback navigation links are always the same three static items
  • Clicking a fallback link navigates and closes the palette

Keyboard Navigation

  [search] auth___                                  [Esc]
  ---------------------------------------------------------
  Initiatives
  > Auth System Overhaul                         [ACTIVE]   <- highlighted
  ---------------------------------------------------------
  Tasks
    Implement auth middleware             [execute] 3/7
  • > indicates the currently highlighted item
  • Arrow Up / Arrow Down — move highlight
  • Enter — navigate to highlighted item, close palette
  • Esc — close palette
  • Type to filter — results update as you type (debounced 100ms)

Result Groups

Group Source Display
Initiatives listInitiatives query Name + status badge
Tasks listTasks query (across initiatives) Name + category + status
Agents listAgents query Name + task description + status badge
Pages Go to Initiatives, Go to Agents, etc. Static navigation items

Groups are hidden when they have 0 matches. Max 5 items per group. Maximum total visible results: 20 (across all groups combined).

Recent Items

Recent navigation history is stored in sessionStorage key cw-cmd-recent as an ordered array of { type, id, label, path } objects (max 10 entries, LRU eviction). Navigating to any entity via the palette pushes it to the front. The "Recent" section only appears when the search query is empty.

  Recent                                   [clock icon per row]
    -> Auth System Overhaul                [initiative]
    -> blue-fox-7                          [agent]
    -> Settings                            [page]

Fuzzy Matching Behavior

  • Uses substring match by default (case-insensitive)
  • Query is split on whitespace: "auth pkce" matches "Implement auth PKCE flow"
  • Each token must appear somewhere in the candidate string (AND logic)
  • No typo tolerance — keep it simple. If fuzzy matching via a library like fuse.js is added later, configure threshold conservatively (0.3) to avoid noise in a mission-control context where precision matters.

Result Ranking

Within each group, results are ordered by:

  1. Exact prefix match — query matches the start of the name (highest)
  2. Substring position — earlier match index ranks higher
  3. Recency — more recently updated items break ties (updatedAt DESC)

Section Keyboard Shortcuts

Key Action
Arrow Up / Arrow Down Move highlight between items (wraps around)
Enter Navigate to highlighted item, close palette
Esc Close palette
Backspace on empty query Close palette
Cmd+K / Ctrl+K while open Close palette (toggle behavior)

Overlay Behavior

  • Renders as a centered modal with backdrop (bg-black/50)
  • Width: max-w-lg (512px)
  • Position: top-[20%] of viewport
  • Backdrop click closes the palette
  • Focus is trapped inside the modal while open
  • Search input auto-focuses on open

Source

  • packages/web/src/components/CommandPalette.tsx (proposed)

5. <ThemeToggle>

3-state segmented control for switching between light, system, and dark color schemes. Persists selection to localStorage.

Props

// No props — reads/writes theme from ThemeProvider context
// ThemeProvider wraps the app root
type Theme = 'light' | 'system' | 'dark';

States

Light active:    [*sun*] [monitor] [moon]
System active:   [sun] [*monitor*] [moon]
Dark active:     [sun] [monitor] [*moon*]

Detailed ASCII

Light mode selected

+-----+----------+---------+
| sun | monitor  |  moon   |
+-----+----------+---------+
  ^^^
  filled bg, active text

System mode selected (default)

+-----+----------+---------+
| sun | monitor  |  moon   |
+-----+----------+---------+
        ^^^^^^^^
        filled bg, active text

Dark mode selected

+-----+----------+---------+
| sun | monitor  |  moon   |
+-----+----------+---------+
                    ^^^^^^
                    filled bg, active text

Styling

  • Container: inline-flex rounded-lg bg-muted p-0.5 gap-0.5
  • Segment (inactive): px-2 py-1 rounded-md text-muted-foreground hover:text-foreground
  • Segment (active): px-2 py-1 rounded-md bg-background text-foreground shadow-sm
  • Icons: 16px lucide-react icons (Sun, Monitor, Moon)
  • No text labels — icons only in the header (compact)

Tooltip on hover

  [sun]     -> "Light"
  [monitor] -> "System (Dark)"     or "System (Light)" — reflects resolved preference
  [moon]    -> "Dark"

The System tooltip dynamically resolves the current prefers-color-scheme media query to show the effective mode. This eliminates user confusion about what "System" actually means on their machine. Read isDark from the ThemeProvider context to determine the parenthetical label.

Persistence

  • Stored in localStorage key cw-theme
  • Default: system
  • On load: reads localStorage, applies class="dark" to <html> if theme is dark or if theme is system and prefers-color-scheme: dark

Usage

Page Location
App layout (header) Right cluster, between Cmd+K button and health dot

See app-layout.md for header placement.

Source

  • packages/web/src/components/ThemeToggle.tsx (proposed)
  • packages/web/src/providers/ThemeProvider.tsx (proposed)

6. <StatusDot>

Semantic status indicator rendered as a small colored circle. Used pervasively across the UI for agents, tasks, health, conversations, and previews. This is the single most repeated visual element in the app — it needs to be a proper component, not ad-hoc <span className="h-2 w-2 rounded-full bg-..." /> copy-pasted everywhere.

Props

interface StatusDotProps {
  /**
   * Maps to a status token from the theme (status-active-dot, status-success-dot, etc.).
   * This is the semantic status, not a color — the theme resolves the actual hue.
   */
  status: 'active' | 'success' | 'warning' | 'error' | 'neutral' | 'urgent';
  /**
   * Dot diameter. Defaults to 'md'.
   * - 'sm' (6px): inline with text, badges, compact lists
   * - 'md' (8px): agent cards, task rows, sidebar items
   * - 'lg' (10px): health indicator in header, large card headers
   */
  size?: 'sm' | 'md' | 'lg';
  /**
   * When true, applies a pulsing opacity animation (0.4 -> 1.0, 2s cycle).
   * Use for statuses that represent ongoing activity (running agent, building preview).
   * Default: false.
   *
   * Animation uses opacity only — no scale — to avoid layout shifts in tight layouts
   * like agent cards. The keyframes:
   *   0% { opacity: 1 }
   *   50% { opacity: 0.4 }
   *   100% { opacity: 1 }
   */
  pulse?: boolean;
  /**
   * Accessible label for screen readers. Required.
   * Examples: "Running", "Crashed", "Connected", "Blocked"
   *
   * Rendered as `aria-label` on the element and `role="status"`.
   * Status should never be communicated through color alone —
   * this label is the accessible equivalent.
   */
  label: string;
}

Size Variants

  sm (6px):   *          <- inline with text runs
  md (8px):   (*)        <- default, agent cards
  lg (10px):  ( * )      <- header health dot

Visual Rendering

+--------------------------------------------+
| Active (pulse):    (*) ← opacity animation |
| Success (static):  (v) ← solid green       |
| Warning (static):  (?) ← solid amber       |
| Error (static):    (!) ← solid red         |
| Neutral (static):  (-) ← solid grey        |
| Urgent (pulse):    (!) ← pulsing purple    |
+--------------------------------------------+

Color Mapping (from theme tokens)

Status Token Typical Color Default Pulse?
active status-active-dot Blue Yes (running agents, building previews)
success status-success-dot Green No
warning status-warning-dot Amber No
error status-error-dot Red No
neutral status-neutral-dot Grey No
urgent status-urgent-dot Purple Yes (blocked tasks, urgent inbox)

Note: pulse is a prop, not automatic per status. The table above shows conventional usage. A completed agent does not pulse; a health dot in green state pulses briefly (3s) then stops — that logic lives in the consuming component, not in StatusDot itself.

Styling

.status-dot {
  display: inline-block;
  border-radius: 9999px;              /* fully round */
  flex-shrink: 0;                     /* never collapse in flex layouts */
  background: hsl(var(--status-{status}-dot));
}

.status-dot--sm  { width: 6px; height: 6px; }
.status-dot--md  { width: 8px; height: 8px; }
.status-dot--lg  { width: 10px; height: 10px; }

.status-dot--pulse {
  animation: status-pulse 2s ease-in-out infinite;
}

@keyframes status-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.4; }
}

Usage

Page Context Status Size Pulse?
App header Health indicator success/warning/error lg Green: 3s then stop
Agents Agent card status active/warning/error/neutral md active: yes
Execution tab Phase/task status active/success/warning/neutral md active: yes
Inbox Conversation priority urgent/warning/neutral/success md urgent: yes
Dialogs Task dependency status varies sm No
Settings Account health success/warning sm No

Accessibility

  • role="status" + aria-label on every instance
  • Color is never the sole indicator — always paired with text label or icon in context
  • Pulse animation respects prefers-reduced-motion: reduce (disable animation)

Source

  • packages/web/src/components/StatusDot.tsx (proposed)

7. <SkeletonLoader>

Shimmer placeholder blocks shown during data loading. Mirrors the anatomy of the content it replaces to prevent layout shift. Used on every data-fetching page.

Props

interface SkeletonProps {
  /**
   * Shape variant:
   * - 'line': horizontal bar (text placeholder). Default height 16px.
   * - 'circle': avatar / status dot placeholder. Uses `size` for diameter.
   * - 'rect': block placeholder (card, image area). Uses `width` x `height`.
   */
  variant?: 'line' | 'circle' | 'rect';
  /** Width. For 'line': defaults to '100%'. For 'circle': ignored (uses size). */
  width?: string | number;
  /** Height. For 'line': defaults to 16px. For 'circle': ignored (uses size). */
  height?: string | number;
  /** Diameter for 'circle' variant. Defaults to 32px. */
  size?: number;
  /** Additional className for width/margin overrides. */
  className?: string;
}

/**
 * Composite skeleton that renders N skeleton cards matching a specific
 * page's card anatomy. Use instead of manually assembling Skeleton primitives.
 */
interface SkeletonCardProps {
  /** Number of skeleton cards to render. Default: 5. */
  count?: number;
  /** Card layout variant matching the page being loaded. */
  layout: 'agent-card' | 'initiative-card' | 'conversation-card' | 'project-card' | 'account-card';
}

Shimmer Animation

All skeleton elements share a single shimmer sweep: a translucent highlight gradient that moves left-to-right on a 1.5s infinite loop. This uses a CSS background-position animation on a linear gradient, not a pseudo-element overlay, so it works on any shape.

Resting:    ░░░░░░░░░░░░░░░░░░░░
Shimmer:    ░░░░░▓▓▓░░░░░░░░░░░░   <- highlight sweeps right
Shimmer:    ░░░░░░░░░░░░▓▓▓░░░░░
.skeleton {
  background: linear-gradient(
    90deg,
    hsl(var(--muted)) 25%,
    hsl(var(--muted-foreground) / 0.08) 50%,
    hsl(var(--muted)) 75%
  );
  background-size: 200% 100%;
  animation: shimmer 1.5s ease-in-out infinite;
  border-radius: var(--radius);
}

@keyframes shimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

Uses --duration-glacial (500ms) as the animation timing reference from the theme spec, though the actual cycle is 1.5s (3x glacial) for a gentler feel.

Composite Card Skeletons

Agent card skeleton (matches agents.md card anatomy):

+---------------------------+
| [.] ░░░░░░░░░░░░         |   <- circle (8px) + line (60%)
|   ░░░░░ . ░░░░░░         |   <- line (40%) + dot + line (50%)
|   ░░░░░░░░░░░░░          |   <- line (70%)
|   ░░░░          ░░░ ░░░  |   <- line (30%) + gap + line (20%) + line (20%)
+---------------------------+

Initiative card skeleton (matches initiatives-list.md card anatomy):

+---------------------------+
| ░░░░░░░░░░░░░░░░  ░░ ░░  |   <- line (65%) + badge + badge
| ░░░░░░░░░░  ░░░░░        |   <- line (40%) + line (30%)
| ░░░░░░░░░░░░░░░░░░░░░░░  |   <- rect (100% x 6px) progress bar
| ░░░░░░░░    ░░░░░  ░░░░  |   <- line (35%) + line (25%) + line (20%)
+---------------------------+

Conversation card skeleton (matches inbox.md card anatomy):

+---------------------------+
| [.] ░░░░░░░░░░  ░░░      |   <- circle (8px) + line (55%) + line (15%)
| ░░░░░░░░░░░░░░░░░░       |   <- line (80%)
| ░░░░░░░░░░░░              |   <- line (50%)
+---------------------------+

Loading-to-Content Transition

  • Skeletons fade out: opacity-0 over 150ms (duration-fast)
  • Content fades in: opacity-100 over 150ms
  • No layout jump: skeleton card dimensions must exactly match real card dimensions
  • Container uses min-h to prevent collapse during transition
  • On error: skeletons fade out, <ErrorState> fades in centered

Accessibility

  • aria-hidden="true" on all skeleton elements (they are decorative)
  • role="status" + aria-label="Loading" on the skeleton container
  • Respects prefers-reduced-motion: reduce — disable shimmer, show static grey blocks

Usage

Page Card Layout Count Notes
Initiatives list initiative-card 5 Matches card height to prevent jump
Agents agent-card 5 Output panel shows empty state, not skeleton
Inbox conversation-card 6 List panel only
Settings > Projects project-card 3
Settings > Accounts account-card 3
Initiative detail Custom 1 Two-row header + tab bar + content area
Content tab Custom 1 Sidebar tree + editor area

Source

  • packages/web/src/components/Skeleton.tsx (proposed — primitives)
  • packages/web/src/components/SkeletonCard.tsx (proposed — composite cards)

8. <Toast> (via Sonner)

Global notification toasts for asynchronous feedback. Rendered by the <Toaster> provider at the app root. Individual toasts are fired imperatively via toast(), toast.success(), toast.error(), etc.

This project uses Sonner (already a shadcn/ui dependency). No custom toast component is needed — this spec documents the configuration and usage conventions.

Toaster Configuration

// In app root layout (see app-layout.md)
<Toaster
  position="bottom-right"
  toastOptions={{
    className: 'font-sans text-sm',
    duration: 5000,                    // default auto-dismiss: 5s
  }}
  visibleToasts={3}                    // max 3 visible, older collapse to "+N more"
  richColors                           // enable semantic color variants
  closeButton                          // always show dismiss X
  offset={16}                          // 16px from viewport edge
/>

Toast Variants

Success:    +---------------------------------------+
            |  [v] Account added              [x]  |
            +---------------------------------------+
            bg-status-success-bg, auto-dismiss 5s

Error:      +---------------------------------------+
            |  [!] Failed to delete project   [x]  |
            |      [Retry]                         |
            +---------------------------------------+
            bg-status-error-bg, persistent (no auto-dismiss)

Info:       +---------------------------------------+
            |  [i] Answer sent to blue-fox-7  [x]  |
            +---------------------------------------+
            default bg, auto-dismiss 5s

Warning:    +---------------------------------------+
            |  [!] Account X exhausted,       [x]  |
            |      switched to Y                   |
            +---------------------------------------+
            bg-status-warning-bg, auto-dismiss 5s

With action:+---------------------------------------+
            |  [!] blue-fox-7 crashed         [x]  |
            |      [View Agent]                    |
            +---------------------------------------+
            persistent, action navigates to /agents

Duration Rules

Category Duration Rationale
Success confirmations 5s User glances, moves on
Info / status changes 5s FYI, not blocking
Warnings (account switch, etc.) 5s Notable but not actionable
"All tasks complete" 8s Celebration moment, give it room
"Agent has a question" 8s Needs attention but not emergency
Agent crashed Persistent Action required — must be manually dismissed
Task needs approval Persistent Action required
Errors with retry Persistent User must acknowledge or retry

Critical Event Toasts (fired globally via EventBus)

These fire regardless of the current page (configured in app-layout.md):

Event Toast Call Duration
Agent crashed toast.error("blue-fox-7 crashed", { action: { label: "View", onClick: navigate } }) Persistent
Task needs approval toast("Task ready for approval", { action: { label: "Review", onClick: navigate } }) Persistent
All tasks complete toast.success("Initiative complete — ready for review") 8s
Account exhausted toast.warning("Account X exhausted, switched to Y") 5s
Agent asking question toast("blue-fox-7 has a question", { action: { label: "Answer", onClick: navigate } }) 8s

Stacking Behavior

                                        +---+
   (viewport bottom-right)             | +N |  <- collapsed count
                                        +---+
                              +---------------------------+
                              | Toast 3 (oldest visible)  |
                              +---------------------------+
                              +---------------------------+
                              | Toast 2                   |
                              +---------------------------+
                              +---------------------------+
                              | Toast 1 (newest)          |
                              +---------------------------+
  • Max 3 visible; older toasts collapse into a "+N more" pill
  • New toasts enter from the bottom with --ease-spring (slight overshoot)
  • Dismissed toasts slide out to the right with --ease-in
  • Z-index: var(--z-toast) (60) — above modals, below command palette

Source

  • packages/web/src/components/ui/sonner.tsx (shadcn/ui generated — configure here)
  • Toaster mount point: packages/web/src/App.tsx or root layout

9. <Badge>

Inline status label rendered as a small colored pill. Used for entity status (RUNNING, ACTIVE, BLOCKED), category labels (execute, verify), tab counts, and filter chips.

Props

interface BadgeProps {
  /** Visual variant controlling color scheme. */
  variant:
    | 'active'      // blue bg — running, in_progress, building
    | 'success'     // green bg — completed, connected, done
    | 'warning'     // amber bg — waiting, exhausted, pending_approval
    | 'error'       // red bg — crashed, failed, disconnected
    | 'neutral'     // grey bg — stopped, idle, pending, exited
    | 'urgent'      // purple bg — blocked
    | 'outline'     // border-only, no fill — provider labels, category tags
    | 'secondary';  // muted fill — mode labels, secondary metadata
  /** Badge text content. Rendered uppercase in status badges, as-is for categories. */
  children: React.ReactNode;
  /** Size variant. Default: 'sm'. */
  size?: 'sm' | 'xs';
  /** Optional leading icon (rendered at 12px for xs, 14px for sm). */
  icon?: React.ReactNode;
  /**
   * When true, text is rendered in uppercase with letter-spacing.
   * Use for status badges (RUNNING, ACTIVE). Default: false.
   */
  uppercase?: boolean;
}

Visual Variants

Status badges (uppercase=true):
  [RUNNING]   <- variant="active", blue bg
  [ACTIVE]    <- variant="active", blue bg
  [DONE]      <- variant="success", green bg
  [WAITING]   <- variant="warning", amber bg
  [CRASHED]   <- variant="error", red bg
  [STOPPED]   <- variant="neutral", grey bg
  [BLOCKED]   <- variant="urgent", purple bg
  [QUEUED]    <- variant="active", blue bg

Category badges (uppercase=false, variant="outline"):
  [execute]   <- outline, no fill
  [verify]    <- outline, no fill
  [research]  <- outline, no fill

Provider/mode badges (uppercase=false):
  [claude]    <- variant="outline"
  [execute]   <- variant="secondary"

Tab count badges (size="xs"):
  [*3]        <- variant="active", xs size (agents tab)
  [2]         <- variant="warning", xs size (inbox tab)

Filter chips (with dismiss):
  [running x] <- variant="active" with trailing X button

Size Variants

Size Font Padding Border Radius Line Height
sm (default) text-xs (12px) px-2 py-0.5 rounded-sm (2px) 20px
xs text-[10px] px-1.5 py-px rounded-sm (2px) 16px

Color Mapping (uses status tokens from theme)

Variant Background Text Border
active bg-status-active-bg text-status-active-fg border-status-active-border
success bg-status-success-bg text-status-success-fg border-status-success-border
warning bg-status-warning-bg text-status-warning-fg border-status-warning-border
error bg-status-error-bg text-status-error-fg border-status-error-border
neutral bg-status-neutral-bg text-status-neutral-fg border-status-neutral-border
urgent bg-status-urgent-bg text-status-urgent-fg border-status-urgent-border
outline transparent text-foreground border border-border
secondary bg-secondary text-secondary-foreground none

Entity-to-Variant Mapping

Use this table when deciding which variant to assign. This is the canonical source — do not invent new mappings without adding them here.

Entity State Variant Label
Agent running active RUNNING
Agent waiting_for_input warning WAITING
Agent completed neutral COMPLETED
Agent crashed error CRASHED
Agent stopped neutral STOPPED
Task in_progress active RUNNING
Task completed success DONE
Task pending neutral PENDING
Task pending_approval warning APPROVAL
Task blocked urgent BLOCKED
Task queued active QUEUED
Task failed error FAILED
Initiative active active ACTIVE
Initiative completed success COMPLETED
Initiative archived neutral ARCHIVED
Initiative mode review warning REVIEW
Initiative mode planning secondary PLANNING
Preview building active BUILDING
Preview running success RUNNING
Preview failed error FAILED
Preview stopped neutral STOPPED
Account active success -- (no badge)
Account exhausted warning EXHAUSTED

Accessibility

  • Badge text is readable by screen readers (no aria-hidden)
  • Color is never the sole differentiator — the text label provides meaning
  • Sufficient contrast ratios are ensured by the status token pairs (bg/fg)

Source

  • packages/web/src/components/Badge.tsx (proposed — or extend shadcn/ui badge.tsx)

10. <KeyboardShortcutHint>

Tooltip component that displays the keyboard shortcut for an action. Used in button tooltips, command palette results, and the ? shortcut help overlay.

Props

interface KeyboardShortcutHintProps {
  /** The key combination. Platform-aware: automatically swaps Cmd/Ctrl.
   *  Format: modifier keys joined with '+'. Examples: "Cmd+K", "Shift+N", "Esc", "1"
   */
  keys: string;
  /** Tooltip text shown before the shortcut. e.g., "Search" renders as "Search  ⌘K" */
  label?: string;
  /**
   * Rendering mode:
   * - 'tooltip': renders inside a shadcn Tooltip (default). Wraps `children`.
   * - 'inline': renders as inline <kbd> elements (for command palette results, help overlay).
   */
  mode?: 'tooltip' | 'inline';
  /** The element that triggers the tooltip. Only used in 'tooltip' mode. */
  children?: React.ReactNode;
}

Platform Detection

// Detect platform once at module level
const isMac = navigator.platform?.startsWith('Mac') ||
              navigator.userAgent?.includes('Mac');

// Key symbol mapping
const KEY_SYMBOLS: Record<string, string> = {
  'Cmd':       isMac ? '⌘' : 'Ctrl',
  'Ctrl':      isMac ? '⌃' : 'Ctrl',
  'Alt':       isMac ? '⌥' : 'Alt',
  'Shift':     '⇧',
  'Enter':     '↵',
  'Backspace': '⌫',
  'Esc':       'Esc',
  'Tab':       '⇥',
  'ArrowUp':   '↑',
  'ArrowDown': '↓',
  'ArrowLeft': '←',
  'ArrowRight':'→',
};

Tooltip Mode (default)

Wraps a trigger element with a shadcn <Tooltip>. The tooltip content shows label (if provided) followed by the key combination in <kbd> pills.

Hovering over the Cmd+K button:

  +-----+
  | ⌘K  |    <- trigger element (the button)
  +-----+
      |
  +-------------+
  | Search  ⌘ K |   <- tooltip: label + kbd pills
  +-------------+
Hovering over a tab:

  +----------+
  | Content  |    <- trigger (tab)
  +----------+
      |
  +-----------+
  | Content 1 |   <- tooltip: label + key
  +-----------+

Inline Mode

Renders <kbd> pills directly in the flow. Used in command palette results and the keyboard shortcut help overlay.

Command palette result with shortcut:

  > Go to Agents                                    ⌘ 2
                                                    ^^^
                                                    inline <kbd> pills

Shortcut help overlay row:

  Next file                                         j
  Previous file                                     k
  Toggle resolve                                    r
  Search                                          ⌘ K

<kbd> Pill Styling

Individual key:
  +---+
  | K |    <- single character key
  +---+

  +------+
  | Ctrl |  <- modifier key (text on non-Mac)
  +------+

  +---+
  | ⌘ |    <- modifier key (symbol on Mac)
  +---+
  • Each key segment is a separate <kbd> element
  • <kbd> styling: inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 rounded-sm bg-muted border border-border text-[11px] font-mono text-muted-foreground leading-none
  • Multiple keys separated by gap-0.5 (2px)
  • Modifier+key combinations: K (two separate pills, no + separator)

Usage

Location Mode Keys Label
Header Cmd+K button tooltip Cmd+K "Search"
Tab tooltips (1-4) tooltip 1, 2, 3, 4 Tab name
Command palette results inline varies --
? shortcut help overlay inline all shortcuts Action name
Inbox send button inline Cmd+Enter --
Plan tab add task tooltip N "New task"

Accessibility

  • <kbd> elements are semantic HTML — screen readers announce them as keyboard input
  • Tooltips use role="tooltip" via shadcn Tooltip
  • Platform-specific symbols (⌘, ⌃, ⌥) have appropriate aria-label fallbacks (aria-label="Command", aria-label="Control", aria-label="Option")

Source

  • packages/web/src/components/KeyboardShortcutHint.tsx (proposed)

Cross-Reference Index

Quick lookup of which shared components are used on each page.

Component app-layout initiatives-list initiative-detail content-tab plan-tab execution-tab review-tab agents inbox settings
SaveIndicator x x
EmptyState x x x x x x
ErrorState x x x x x x
CommandPalette x
ThemeToggle x
StatusDot x x x x x
SkeletonLoader x x x x x x
Toast x x x x
Badge x x x x x x
KeyboardShortcutHint x x x x x x x x

Design Review Notes

Existing components: what was already solid

  1. SaveIndicator — the persistent prop with timestamp display is exactly right. High-stakes contexts (page content editing) need ongoing confirmation; lightweight contexts (phase descriptions) do not. The Intl.DateTimeFormat call is a good detail. One nit: consider adding a fadeDuration prop (default 2000ms) for contexts where the fade timing needs tuning. Not critical — the current hardcoded 2s is fine for now.

  2. EmptyState — the dual-action pattern with primary/secondary buttons is well spec'd. The button variant assignments (default vs outline) are correct. The usage table covering 7 pages with specific copy is useful for implementation. The "without description" variant prevents over-engineering simple empty states.

  3. ErrorState — the expandable details section with <details>/<summary> is the right call. Using native HTML disclosure is simpler than a state toggle and works without JS. The errorCode displayed in font-mono is good for bug reports. One gap: there is no "Copy error details" button for users filing issues. Consider adding a clipboard-copy icon inside the details panel.

  4. CommandPalette — this section is thorough. The fuzzy matching behavior (whitespace-split AND-logic, no typo tolerance) is a good conservative default for a precision-oriented tool. The result ranking (prefix > position > recency) is sound. Recent items in sessionStorage with LRU eviction is correct — these should not persist across sessions. The "no results" fallback with static navigation links prevents dead ends.

  5. ThemeToggle — the dynamic "System (Dark)" / "System (Light)" tooltip resolving prefers-color-scheme is the right UX. Users who pick "System" and see dark mode need to know it is working as intended, not broken. Reading isDark from ThemeProvider context is the clean implementation path.

Added components: rationale

  1. StatusDot — this was the most glaring omission. The dot appears on every data-driven page, in at least 6 different contexts, and was being described ad-hoc in agents.md, app-layout.md, inbox.md, execution-tab.md, and dialogs.md. Without a shared spec, each page would implement its own dot with slightly different sizes, animations, and accessibility. The pulse prop is intentionally not automatic per status — the consuming component decides (e.g., health dot pulses for 3s then stops; agent dot pulses indefinitely). The label prop is required, not optional, because status-by-color-alone fails WCAG.

  2. SkeletonLoader — every wireframe describes its own skeleton layout but none referenced a shared primitive. The composite SkeletonCard pattern (with named layouts like agent-card, initiative-card) prevents skeleton/content dimension mismatches that cause layout jumps. The shimmer animation is standardized at 1.5s with theme token references.

  3. Toast — app-layout.md already described Sonner configuration and critical event toasts, but scattered across two sections. Centralizing the duration rules, stacking behavior, and variant usage in one place prevents inconsistency. The duration table (5s default, 8s for attention, persistent for action-required) is a policy decision that should live in a shared spec, not be reinvented per page.

  4. Badge — the most polymorphic component in the system. It appears as status badges (RUNNING), category labels (execute), tab counts (*3), provider labels (claude), and filter chips. Without a canonical variant-to-color mapping, every developer picks their own colors. The entity-to-variant table is the single source of truth — if a status is not in that table, it does not get a badge.

  5. KeyboardShortcutHint — a keyboard-first tool that does not show its shortcuts is just a tool. The <kbd> pill styling with platform-aware symbols (⌘ vs Ctrl) is table stakes. The dual mode (tooltip wrapping trigger elements vs inline in command palette results) covers both discovery patterns. The aria-label fallbacks on Mac symbols prevent screen readers from announcing "unknown character."

Open questions for implementation

  • StatusDot pulse and prefers-reduced-motion: The spec says disable animation, but should it fall back to a static solid dot or a subtly different visual (e.g., a ring outline)? A static dot for a running agent looks identical to a completed agent to users with reduced-motion preferences. Recommendation: use a double-ring outline as the reduced-motion fallback for pulse=true.

  • Badge filter chips: The spec mentions [running x] with a dismiss button but does not fully define the dismiss interaction. This depends on the filter bar implementation in initiatives-list.md and agents.md. Defer the dismissible badge variant until those filter patterns are finalized.

  • SkeletonCard dimension matching: The spec says skeleton dimensions must match real card dimensions. In practice, this requires the skeleton and card to share a CSS class or dimension constant. Consider extracting card height values into CSS custom properties (e.g., --card-height-agent: 112px) referenced by both the real card and its skeleton.