# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview Multi-agent chat interface deployed to Cloudflare Workers. Users select from configured AI agents and chat with them through n8n webhooks. The interface features a glass-morphism design with mobile support, markdown rendering with code highlighting, and a custom diff visualization tool. ## Key Commands ### Development ```bash pnpm dev # Start Next.js dev server at localhost:3000 pnpm build # Build Next.js application pnpm lint # Run ESLint checks pnpm test # Run Vitest test suites pnpm test:ui # Run tests with Vitest UI pnpm test:coverage # Generate test coverage report ``` ### Deployment to Cloudflare ```bash npx @opennextjs/cloudflare build # Convert Next.js build to Cloudflare Workers format npx wrangler deploy # Deploy to Cloudflare (requires OpenNext build first) npx wrangler tail # View live logs from deployed worker ``` **Critical**: - Always run `npx @opennextjs/cloudflare build` before deploying. The standard `next build` alone is insufficient for Cloudflare deployment. - Always use `npx wrangler deploy` without any `--env` flags. The deployment uses the default configuration from `wrangler.jsonc` with route `agents.nicholai.work`. ## Architecture ### Agent Configuration - Agents are defined via environment variables: `AGENT_N_URL`, `AGENT_N_NAME`, `AGENT_N_DESCRIPTION` (where N = 1, 2, 3...) - `/api/agents` dynamically discovers agents by iterating through numbered environment variables - Each agent has a webhook URL pointing to an n8n workflow - Agent selection is persisted to localStorage using agent-specific keys ### Message Flow 1. User submits message through `ChatInterface` component 2. POST `/api/chat` receives message with agentId, sessionId, timestamp, and optional base64 images 3. Route extracts webhook URL from environment variables based on agentId format (`agent-N`) 4. Message forwarded to n8n webhook with all metadata 5. Response parsed from n8n (supports streaming chunks and diff tool calls) 6. Messages stored in localStorage per agent (`chat-messages-{agentId}`) ### Session Management - Each agent has its own session: `chat-session-{agentId}` in localStorage - Session ID format: `session-{agentId}-{timestamp}-{random}` - New session created when user clicks "Start a fresh conversation" - Messages persist across page reloads per agent ### Diff Tool Integration - Custom markdown code fence: ` ```diff-tool ` with JSON payload - n8n can send `type: "tool_call", name: "show_diff"` which is converted server-side - `MarkdownRenderer` parses diff-tool blocks and renders via `DiffTool` → `DiffDisplay` components - Diff props: `oldCode`, `newCode`, `title`, `language` ## Code Structure ### API Routes - `src/app/api/agents/route.ts` - Discovers and returns configured agents from environment variables - `src/app/api/chat/route.ts` - Proxies chat messages to agent webhooks, handles streaming responses and diff tool calls ### Core Components - `src/app/page.tsx` - Main page with agent selection/persistence and chat interface mounting - `src/components/chat-interface.tsx` - Full chat UI: message history, input composer, agent dropdown, image attachments - `src/components/markdown-renderer.tsx` - Renders markdown with syntax highlighting and custom diff-tool blocks - `src/components/diff-tool.tsx` - Wrapper for diff display functionality - `src/components/diff-display.tsx` - Side-by-side diff visualization ### Styling - Tailwind CSS 4.x with custom glass-morphism design system - Custom colors defined in CSS variables (charcoal, burnt, terracotta, etc.) - Mobile-specific classes: `mobile-shell`, `mobile-feed`, `mobile-composer` - Framer Motion for animations and layout transitions ### Type Definitions - `src/lib/types.ts` - All TypeScript interfaces for Agent, Message, API requests/responses ## Important Patterns ### Environment Variable Access Always extract agent index from agentId before accessing env vars: ```typescript const match = agentId.match(/agent-(\d+)/) const agentIndex = match[1] const webhookUrl = process.env[`AGENT_${agentIndex}_URL`] ``` ### LocalStorage Keys - Agent selection: `selected-agent` (full object), `selected-agent-id` (string) - Per-agent session: `chat-session-{agentId}` - Per-agent messages: `chat-messages-{agentId}` ### Response Parsing in /api/chat The route handles multiple n8n response formats: 1. Streaming chunks (newline-delimited JSON with `type: "item"`) 2. Tool calls (`type: "tool_call", name: "show_diff"`) 3. Regular JSON with various fields (`response`, `message`, `output`, `text`) 4. Plain text fallback ### Image Handling - Images converted to base64 via FileReader in browser - Passed as `images: string[]` array to `/api/chat` - Forwarded to n8n webhook for processing - Preview thumbnails shown in composer with remove buttons ## Deployment Configuration ### wrangler.jsonc - Worker entry: `.open-next/worker.js` (generated by OpenNext) - Assets directory: `.open-next/assets` - Route: `agents.nicholai.work` (configured at top level, not in env section) - Environment variables: Configured in top-level `vars` section - Compatibility flags: `nodejs_compat`, `global_fetch_strictly_public` - **Important:** Deployment uses default configuration - do not use `--env` flags ### Build Output - Standard Next.js build: `.next/` - Cloudflare-adapted build: `.open-next/` (generated by @opennextjs/cloudflare) - Only `.open-next/` is deployed to Cloudflare ## UI/UX Details ### Empty State - Hero greeting animation with per-character stagger - Agent selection buttons shown prominently - Sample prompt cards (4 suggestions) users can click to populate input ### Composer Behavior - Auto-expanding textarea (max 224px height) - Agent dropdown required before sending first message - Agent selection highlighted if not yet chosen - Enter to send, Shift+Enter for new line - Paperclip icon for image attachments ### Top Button Layout - "Start new chat" button positioned on left side of manuscript panel - "View pinned agents" bookmark button positioned on right side - Uses flex layout with `ml-auto` to push bookmark to the right ### Message Display - User messages: positioned on right with glass bubble styling, text left-aligned within bubble (natural flow) - Message content limited to 75% max-width with word-wrapping for long text - Code blocks in messages have horizontal scrolling with proper overflow handling - Assistant messages: markdown-rendered with copy button and syntax highlighting - Error messages: red text, optional hint field displayed below - Loading state: animated shimmer bar - Scrollbars: gray color (neutral), hidden by default, shown on hover ### Pinned Agents Drawer **Mobile Design:** - Full-width bottom sheet sliding up from bottom - Drag handle indicator visible at top - Semi-transparent dark background - Responsive layout for touch interaction **Desktop Design:** - Side drawer that slides out from behind the main manuscript card to the right - Positioned relative to the manuscript panel (not viewport-locked) - Partially hidden behind main card, with left edge not visible - Shorter height, centered vertically on screen - Reduced opacity (40% less) for subtle appearance **Card Functionality:** - Minimalist cards showing: agent name, handle, and start chat button - Hover reveals: full description and action button - Smooth height animation on hover using CSS Grid transitions - Drag-and-drop reordering with visual feedback - Dragged cards render above others (z-index layering) - Delete button only visible on hover ## Feature Flag System ### Architecture - Flags defined in `src/lib/flags.ts` with TypeScript interfaces - Environment variable based: `IMAGE_UPLOADS_ENABLED`, `DIFF_TOOL_ENABLED` - Runtime overrides supported via `registerRuntimeFlags()` (useful for testing) - Global caching in `globalThis.__MULTI_AGENT_CHAT_FLAGS__` - Client-side access via `useFlags()` hook that fetches from `/api/flags` ### Available Flags - `IMAGE_UPLOADS_ENABLED` (default: true) - Controls image attachment UI and API validation - `DIFF_TOOL_ENABLED` (default: true) - Controls diff visualization rendering ### Flag Checking Patterns **Server-side (API routes):** ```typescript import { getFlags } from '@/lib/flags' const flags = getFlags() if (!flags.IMAGE_UPLOADS_ENABLED) { return NextResponse.json({ error: 'Feature disabled' }, { status: 403 }) } ``` **Client-side (Components):** ```typescript import { useFlags } from '@/lib/use-flags' const { flags, isLoading, error } = useFlags() if (flags.IMAGE_UPLOADS_ENABLED) { // Render feature UI } ``` **Testing:** ```typescript import { registerRuntimeFlags, resetFlagsCache } from '@/lib/flags' beforeEach(() => { resetFlagsCache() registerRuntimeFlags({ IMAGE_UPLOADS_ENABLED: false }) }) ``` ### Configuration - Development: Set in `.env.local` - Production: Set in `wrangler.jsonc` under `env.production.vars` - Parse format: "true"/"false", "1"/"0", "yes"/"no" (case-insensitive) ## Testing ### Test Framework - **Vitest 4.0** - Fast unit test framework with Jest-compatible API - **@testing-library/react** - React component testing utilities - **jsdom** - Browser environment simulation ### Test Structure Tests organized in `__tests__/` by domain: - `lib/` - Utility and library tests (flags, types, helpers) - `api/` - API route tests with mocked fetch - `components/` - React component rendering tests - `flags/` - Feature flag integration tests ### Running Tests ```bash pnpm test # Run all tests in watch mode pnpm test:ui # Open Vitest UI dashboard pnpm test:coverage # Generate coverage report ``` ### Test Patterns **API Route Testing:** ```typescript import { POST } from '@/app/api/chat/route' import { NextRequest } from 'next/server' const request = new NextRequest('http://localhost/api/chat', { method: 'POST', body: JSON.stringify({ message: 'test', agentId: 'agent-1' }) }) const response = await POST(request) ``` **Component Testing:** ```typescript import { render, screen } from '@testing-library/react' import { MarkdownRenderer } from '@/components/markdown-renderer' render() expect(screen.getByText('Hello')).toBeInTheDocument() ``` **Flag Behavior Testing:** ```typescript // Test feature when flag is disabled process.env.IMAGE_UPLOADS_ENABLED = 'false' resetFlagsCache() const response = await POST(requestWithImages) expect(response.status).toBe(403) ``` ### Test Configuration - Config: `vitest.config.ts` - Setup: `vitest.setup.ts` (mocks window.matchMedia, IntersectionObserver, etc.) - Path aliases: `@/` resolves to `./src/` - Environment: jsdom with globals enabled - Coverage: v8 provider, excludes node_modules, .next, __tests__ ### Important Testing Notes - Always call `resetFlagsCache()` in beforeEach when testing flags - Mock `global.fetch` for API tests that call webhooks - Use `vi.mock()` for module mocking, `vi.spyOn()` for function spying - Clean up mocks with `vi.clearAllMocks()` in beforeEach - Test both success and error paths for all features ## Development Notes - Next.js 15.5.4 with App Router (all components are Client Components via "use client") - React 19.1.0 with concurrent features - Deployed to Cloudflare Workers, not Vercel - No database - all state in localStorage and n8n workflows - Mobile-first responsive design with specific mobile breakpoint styling - Vitest for testing with Testing Library for React components