# Implementation Tasks — Impeccable Wave

**Dependency map:** Tasks within a phase are sequential (foundation before use). Phases 2–5 can run in parallel after Phase 1 completes. Phase 6 (polish) is last.

```
Phase 1 (Foundation)
  ├── 1.1 Motion tokens
  ├── 1.2 Install framer-motion
  └── 1.3 Create lib/motion.ts
      │
      ├──► Phase 2 (Workspace) ─── 2.1–2.4
      ├──► Phase 3 (Chat) ──────── 3.1–3.4
      ├──► Phase 4 (Identity) ──── 4.1–4.3
      └──► Phase 5 (UX Depth) ──── 5.1–5.5
              │
              └──► Phase 6 (Polish) ─── 6.1–6.5
```

---

## Phase 1 — Foundation (prerequisite for all visual animation)

### 1.1 Define motion design tokens in globals.css

**File:** `apps/web/src/styles/globals.css`

Add `@theme` block with duration, easing, and motion-preset custom properties. Include `motion-safe:` and `motion-reduce:` variants.

**Checklist:**
- [ ] `--duration-instant: 0ms`, `--duration-fast: 100ms`, `--duration-normal: 150ms`, `--duration-slow: 200ms`, `--duration-deliberate: 300ms`
- [ ] `--ease-enter`, `--ease-move`, `--ease-hover`, `--ease-emerge`, `--ease-exit` with cubic-bezier values
- [ ] `--motion-reduce-transform: none` with `prefers-reduced-motion` override
- [ ] Verify no existing hardcoded values conflict

**Verification:** `lsp_diagnostics` clean, `pnpm -C apps/web build` passes.

### 1.2 Install framer-motion

```bash
pnpm -C apps/web add framer-motion
```

**Verification:** `pnpm -C apps/web build` exits 0. Bundle size: check `pnpm -C apps/web build --report` for framer-motion contribution.

### 1.3 Create `lib/motion.ts` — JS motion constants

**File:** `apps/web/src/lib/motion.ts`

Export framer-motion `Transition` config objects referencing the CSS tokens as a single source of truth:

```typescript
export const transitions = {
  fast: { type: 'spring' as const, stiffness: 400, damping: 30 },
  normal: { type: 'spring' as const, stiffness: 300, damping: 25 },
  slow: { type: 'spring' as const, stiffness: 200, damping: 20 },
  enter: { duration: 0.2, ease: [0.22, 1, 0.36, 1] },
  exit: { duration: 0.1, ease: [0.4, 0, 0.2, 1] },
};

export const variants = {
  staggerContainer: { animate: { transition: { staggerChildren: 0.03 } } },
  fadeSlideIn: { initial: { opacity: 0, y: 8 }, animate: { opacity: 1, y: 0 } },
  scaleIn: { initial: { opacity: 0, scale: 0.95 }, animate: { opacity: 1, scale: 1 } },
};
```

**Verification:** Importable in any component. `lsp_diagnostics` clean.

---

## Phase 2 — Workspace Pane Animation (core UX)

### 2.1 Add layout animation to pane wrapper

**File:** `apps/web/src/components/Workspace.tsx`

Wrap each pane in `<motion.div layout>` with enter/exit animations. Use `AnimatePresence` mode="popLayout" around the pane grid. Panes enter with scale+fade from their edge, exit with quick fade.

**Edge cases:**
- First render: no animation for existing panes (use `initial={false}`)
- Single pane with no siblings: no layout shift, skip animation
- Mobile single-column: simplified animation (no edge-direction, just fade)

**Verification:** Open tab, close tab, split pane. Observe smooth transitions. Verify `prefers-reduced-motion` disables all animation.

### 2.2 Animate tab switch in ChatTabBar

**File:** `apps/web/src/components/ChatTabBar.tsx`

Add `layout` prop to tab strip items for reorder animation. New tabs appear with `scaleIn` variant.

**Verification:** Drag-reorder tabs, close tabs, observe smooth reflow.

### 2.3 Animate pane split

**File:** `apps/web/src/components/Workspace.tsx` (split handler)

When splitting a pane, the new pane emerges from the split point with `transform-origin` at the split edge. Uses `scaleIn` variant.

**Verification:** Split any pane. New pane scales in from correct origin.

### 2.4 Add pane drag-reorder animation

**File:** `apps/web/src/components/Workspace.tsx`

Enable framer-motion's `reorder` group on the pane grid. Replace HTML5 native drag with framer-motion `Reorder.Group` for spring-animated reordering.

**Verification:** Drag panes to reorder. Motion is spring-based, not hard snap. Reorder is committed on drop.

---

## Phase 3 — Chat Animation (messages + tool calls)

### 3.1 Add stagger entrance to MessageList

**File:** `apps/web/src/components/MessageList.tsx`

Wrap the message list in `<motion.div variants={staggerContainer}>`. Each message `<motion.div variants={fadeSlideIn}>` staggers in at 30ms intervals.

**Edge cases:**
- Initial load: All existing messages should NOT animate (use `initial={false}` on the container)
- Streaming messages: Only the latest assistant message should animate in when it starts streaming
- Scroll-anchored: When user is at bottom, animate. When scrolled up, suppress animation
- Tool call groups: Animate as a single unit (not individual tool calls)

**Verification:** Open a chat with previous messages — no animation on load. Send a new message — it animates in. Scroll up and send — no animation.

### 3.2 Animate streaming messages (delta updates)

**File:** `apps/web/src/components/MessageBubble.tsx`

Add a subtle pulse/glow animation on the message border while `status === 'streaming'`. The glow transitions from ember orange pulse to steady state on completion.

```tsx
<motion.div
  animate={message.status === 'streaming' ? { borderColor: ['rgba(255,122,24,0)', 'rgba(255,122,24,0.3)', 'rgba(255,122,24,0)'] } : {}}
  transition={message.status === 'streaming' ? { duration: 1.5, repeat: Infinity } : {}}
/>
```

**Edge cases:** Must stop immediately on completion. Must respect reduced motion (no pulse, just steady border).

**Verification:** Send a message, observe ember pulse. Message completes, pulse stops. Reduce motion enabled — no pulse.

### 3.3 Add spring animation to tool call expansion

**File:** `apps/web/src/components/ToolCallLine.tsx`

Replace the CSS grid-rows collapse with framer-motion `AnimatePresence` + spring height transition. The expanded content slides down from the collapsed header.

```tsx
<AnimatePresence initial={false}>
  {expanded && (
    <motion.div
      initial={{ height: 0, opacity: 0 }}
      animate={{ height: 'auto', opacity: 1 }}
      exit={{ height: 0, opacity: 0 }}
      transition={{ type: 'spring', stiffness: 300, damping: 30 }}
    />
  )}
</AnimatePresence>
```

**Verification:** Click tool call to expand — smooth spring transition. Click to collapse — quick exit. Multiple rapid clicks — interruptible.

### 3.4 Animate reasoning block open

**File:** `apps/web/src/components/MessageBubble.tsx` (ReasoningBlock)

Same pattern as tool call expansion — spring-based height transition instead of the current grid-rows CSS trick. Reasoning icon rotates (ChevronRight → ChevronDown) with a spring twist.

**Verification:** Click reasoning block header — smooth expand. Content appears without delay. Collapse — spring exit.

---

## Phase 4 — Visual Identity (fonts + empty states + atmosphere)

### 4.1 Swap font pairing

**Files:** `apps/web/src/main.tsx`, `apps/web/src/styles/globals.css`

- Choose new sans font (e.g. Instrument Sans or Public Sans from Google Fonts)
- Add `@fontsource-variable/instrument-sans` import in `main.tsx`
- Update `--font-sans` in `globals.css` to `"Instrument Sans Variable", ...`
- Verify no layout shift during font swap (font-display: swap)

**Verification:** All UI text renders in new font. Code renders in JetBrains Mono (unchanged). No flash of invisible text (FOIT). No CLS.

### 4.2 Create reusable EmptyState component

**File:** `apps/web/src/components/EmptyState.tsx`

A reusable component as designed in `design.md` §9. Accepts `icon` (React node), `title`, `description?`, `action?`.

**Edge cases:**
- No icon provided — use a default muted circle
- Very long description — truncate with `line-clamp-2`
- Icon is text node — wrap in decorative container

### 4.3 Replace text-only empty states with illustrated versions

**Files:** `apps/web/src/pages/Home.tsx`, `apps/web/src/pages/Project.tsx`, `apps/web/src/components/SessionLandingPage.tsx`

Replace "No projects" / "No sessions" / "No chats" plain text with `<EmptyState>` components. Use inline SVGs for illustrations (not external assets).

**Illustrations needed:**
- Home empty: Terminal cursor + code bracket icon
- Project empty: Folder + glow dot
- Session empty: Chat bubble + ember spark

**Verification:** Navigate to each page with no data. See illustrated empty state. Action buttons work.

---

## Phase 5 — UX Depth (undo, keyboard reference, code splitting)

### 5.1 Create keyboard shortcut constant + dialog

- **File (constants):** `apps/web/src/lib/keyboard-shortcuts.ts`
- **File (dialog):** `apps/web/src/components/KeyboardShortcutsDialog.tsx`

Define all shortcuts as typed constants. Dialog renders a modal with grouped sections. Triggered by `Cmd+/` or `?` in `Session.tsx`. Shortcut keys render with `<kbd>` styling.

**Edge cases:** Mobile — shortcuts dialog shows touch-equivalent actions. macOS vs Windows — show `Cmd` vs `Ctrl` labels.

**Verification:** Press `Cmd+/` — dialog opens. Esc closes. Keys are accurate. `kbd` elements are keyboard-focusable.

### 5.2 Add undo for delete chat

**File:** `apps/web/src/components/MessageBubble.tsx` (delete handler)

Before calling `api.messages.remove`, snapshot the messages. Wrap the delete call in an undo toast. On undo, restore via existing API. Timeout: 5 seconds.

### 5.3 Add undo for archive project

**File:** `apps/web/src/components/ProjectSidebar.tsx` (archive handler)

Same pattern as delete chat. Archive → toast with Undo → unarchive on click.

### 5.4 Add route-level code splitting

**File:** `apps/web/src/App.tsx`

Replace static `import Home from '@/pages/Home'` with `const Home = lazy(() => import('@/pages/Home'))`. Add `Suspense` boundary per route with a `FullPageLoader` fallback. Configure `manualChunks` in `vite.config.ts`.

**Checklist:**
- [ ] All 6 page components switched to `lazy()`
- [ ] `Suspense` with `FullPageLoader` (existing skeleton pattern)
- [ ] `vite.config.ts` `build.rollupOptions.output.manualChunks` for vendor + UI chunk
- [ ] Verify named exports work (if pages use `export default`)
- [ ] Build and check chunk output

**Verification:** `pnpm -C apps/web build` produces separate chunk files. Initial page load faster (measure via DevTools network tab).

### 5.5 Add optional suspense for heavy components

**File:** `apps/web/src/components/MarkdownRenderer.tsx`, `apps/web/src/components/CodeBlock.tsx`

Wrap heavy rendering (react-markdown, Shiki highlight) in `<Suspense>` with inline skeleton fallback. The Shiki highlighting in `CodeBlock.tsx` is async — add `lazy()` at component level.

---

## Phase 6 — Polish Pass (critique close-out)

### 6.1 Remove "tap" label from ToolCallGroup

**File:** `apps/web/src/components/ToolCallGroup.tsx`

Delete or comment out the `<span>tap</span>` label. The chevron rotation already communicates expandability.

**Verification:** ToolCallGroup no longer shows "tap" text. Chevron still rotates.

### 6.2 Fix StatusDot spinner origin + reduced-motion guard

**File:** `apps/web/src/components/StatusDot.tsx`

- Add `transform-origin: center` to the spinning dots container
- Add `motion-reduce:hidden` class to the animated dots
- Verify the static fallback is visible (a single amber dot when motion reduced)

**Verification:** StatusDot spins smoothly (no blur). Enable reduced motion — static dot appears.

### 6.3 Fix CompactCard share popover overflow

**File:** `apps/web/src/components/MessageBubble.tsx` (CompactCard share handler)

Replace `absolute right-0 top-full` with a portal-based dropdown using the existing `Dialog` component or a boundary-aware positioning utility.

**Verification:** Share popover never renders past viewport edge, even when trigger is at right edge of message list.

### 6.4 Fix ChatInput toolbar density

**File:** `apps/web/src/components/ChatInput.tsx`

Move Flows, SlashCommands, and WebSearch toggle into an overflow menu (three-dot `MoreHorizontal` button). Keep AgentPicker, Attach, ContextMeter, and Send/Stop always visible.

**Decision:** The overflow menu uses `DropdownMenu` from shadcn (already exists). Desktop shows 4 items always visible + 3 in overflow. Mobile shows same pattern but with larger touch targets.

**Verification:** ChatInput toolbar shows 4 items. "..." button reveals 3 more. All actions work from both positions.

### 6.5 Fix ember user bubble side-stripe

**File:** `apps/web/src/styles/themes/ember.css`

Replace `border-right: 2px solid var(--primary)` with `border-top: 2px solid var(--primary)` for design system compliance. This preserves the accent distinction for user messages without violating the side-stripe ban.

**Verification:** User messages show a top accent border instead of right accent stripe. Looks intentional, not like a leftover.

---

## Final Verification

### Build + typecheck
```bash
pnpm -C apps/server build
pnpm -C apps/web build
pnpm -C apps/web typecheck
```

### LSP diagnostics
```bash
lsp_diagnostics(filePath="apps/web/src", extension=".ts")
lsp_diagnostics(filePath="apps/web/src", extension=".tsx")
```

### Visual QA
1. Open workspace — panes animate in
2. Open chat — messages stagger in
3. Send message — streaming pulse, new message enters
4. Expand tool call — spring animation
5. Press Cmd+/ — shortcuts dialog opens
6. Delete chat — undo toast appears, restore works
7. Reduce motion — all animations stop
8. Switch to mobile viewport — no layout breakage

### Run critique
```bash
$impeccable critique Session
```
Expect score improvement from 35/40 to 37+/40.
