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

13 KiB
Raw Blame History

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

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:

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.

<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.

<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

pnpm -C apps/server build
pnpm -C apps/web build
pnpm -C apps/web typecheck

LSP diagnostics

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

$impeccable critique Session

Expect score improvement from 35/40 to 37+/40.