Files
boocode/openspec/changes/impeccable-wave/tasks.md
indifferentketchup 6fde7002aa docs: boocode-lift-analysis, openspec change docs, codesight cache, deps
- Add boocode-lift-analysis.md: comprehensive 30-repo lift matrix across 25 domains
- Add openspec/ change docs: domain2-code-intelligence, domain3-multi-agent, impeccable-wave, streaming-codeblocks
- Update .gitignore: .impeccable/, .omo/, bun.lock, DESIGN.md, PRODUCT.md
- Update dependencies in package.json + pnpm-lock.yaml
- Update .codesight/ analysis cache
2026-06-08 03:49:26 +00:00

341 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Implementation Tasks — Impeccable Wave
**Dependency map:** Tasks within a phase are sequential (foundation before use). Phases 25 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.12.4
├──► Phase 3 (Chat) ──────── 3.13.4
├──► Phase 4 (Identity) ──── 4.14.3
└──► Phase 5 (UX Depth) ──── 5.15.5
└──► Phase 6 (Polish) ─── 6.16.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.