# 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 Deployment Checklist (must be followed exactly):** 1. **Never use npm.** Always run commands with pnpm (e.g., `pnpm install`, `pnpm dev`, `pnpm build`). 2. **Run the standard Next.js production build:** ```bash pnpm build ``` Do not skip this step—it’s the fastest way to catch type or compile issues before packaging for Cloudflare. 3. **Fix lint errors before deploying:** ```bash pnpm lint ``` Resolve every reported error (warnings are acceptable) before proceeding so Cloudflare builds don’t fail mid-process. 4. **Before every deploy, run the Cloudflare build step:** ```bash npx @opennextjs/cloudflare build ``` If this fails, fix the error before proceeding—Wrangler deploys without this step will push stale assets. 5. **Deploy only after a successful OpenNext build:** ```bash npx wrangler deploy ``` **Do NOT use `--env` flags.** The deployment uses the default configuration from `wrangler.jsonc` with route `agents.nicholai.work`. **Troubleshooting:** - "No updated asset files to upload": you skipped the Cloudflare build; rerun step 2 - ESLint config errors during build: they're informational—build still succeeds, but address them separately - Viewport metadata warning: move viewport values from metadata to generateViewport per Next.js docs **No deploy is compliant unless each step above is completed and verified.** ## Architecture ### Agent Configuration - **Standard agents** defined via environment variables: `AGENT_N_URL`, `AGENT_N_NAME`, `AGENT_N_DESCRIPTION` (where N = 1, 2, 3...) - **Custom agents** use special environment variables: - `CUSTOM_AGENT_WEBHOOK` - n8n webhook for custom agent message processing - `CUSTOM_AGENT_REGISTRATION_WEBHOOK` - n8n webhook for storing agent prompts - `/api/agents` dynamically discovers standard 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 - Custom agents (created via Morgan) have `agentId` format: `custom-{uuid}` ### 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: - Standard agents (`agent-N`): Uses `AGENT_N_URL` from environment variables - Custom agents (`custom-*`): Uses `CUSTOM_AGENT_WEBHOOK` from environment variables 4. Message forwarded to n8n webhook with all metadata (including systemPrompt for custom agents) 5. Response parsed from n8n (supports streaming chunks, tool calls, and regular messages) 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` ### Agent Forge Feature - **Morgan (Agent Architect)** - Typically `agent-2`, creates custom agents users can pin or use immediately - System prompt: `.fortura-core/web-agents/agent-architect-web.md` - **Custom Agent Creation Flow:** 1. User asks Morgan to create an agent 2. Morgan outputs `messageType: "tool_call"` with `create_agent_package` payload 3. Client displays `AgentForgeCard` with animated reveal 4. User actions: **Use now** (registers + switches), **Pin for later** (saves to localStorage), **Share** (copies to clipboard) - **Storage:** Custom agents stored in localStorage `pinned-agents` array with structure: ```typescript { agentId: "custom-{uuid}", displayName: string, summary: string, tags: string[], systemPrompt: string, recommendedIcon: string, whenToUse: string, pinnedAt: string, note?: string } ``` - Custom agents use `CUSTOM_AGENT_WEBHOOK` instead of numbered agent URLs - Registration webhook (`CUSTOM_AGENT_REGISTRATION_WEBHOOK`) stores prompts when user pins/uses agents ## Code Structure ### API Routes - `src/app/api/agents/route.ts` - Discovers and returns configured agents from environment variables - `src/app/api/agents/create/route.ts` - Registers custom agents with n8n via registration webhook - `src/app/api/chat/route.ts` - Proxies chat messages to agent webhooks, handles streaming responses and diff tool calls - `src/app/api/flags/route.ts` - Returns feature flags from environment variables ### 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/agent-forge-card.tsx` - Displays custom agent creation UI with animated reveal - `src/components/pinned-agents-drawer.tsx` - Sliding drawer for managing pinned custom agents - `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 check agent type and extract appropriate webhook URL: ```typescript // For standard agents const match = agentId.match(/agent-(\d+)/) if (match) { const agentIndex = match[1] const webhookUrl = process.env[`AGENT_${agentIndex}_URL`] } // For custom agents if (agentId.startsWith('custom-')) { const webhookUrl = process.env.CUSTOM_AGENT_WEBHOOK } ``` ### LocalStorage Keys - Agent selection: `selected-agent` (full object), `selected-agent-id` (string) - Per-agent session: `chat-session-{agentId}` - Per-agent messages: `chat-messages-{agentId}` - Custom agents: `pinned-agents` (array of PinnedAgent objects) ### 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"`) 3. Code node output (`[{ output: { messageType, content, toolCall? } }]`) 4. Regular JSON with various fields (`response`, `message`, `output`, `text`) 5. Plain text fallback ### n8n Response Format Requirements **CRITICAL:** For proper tool call handling (especially Morgan), n8n workflows must output in this exact format: ```json [{ "output": { "messageType": "regular_message" | "tool_call", "content": "Message text (always present)", "toolCall": { // Only when messageType is "tool_call" "type": "tool_call", "name": "create_agent_package" | "show_diff", "payload": { // Tool-specific data structure } } } }] ``` **Implementation in n8n:** - Use a **Code node** (NOT structured output parser) after the LLM node - The Code node unwraps nested `output` fields and ensures clean JSON: ```javascript const llmOutput = $input.item.json.output || $input.item.json; let actual = llmOutput; while (actual.output && typeof actual.output === 'object') { actual = actual.output; // Unwrap nested output } return { json: { output: actual } }; ``` **Morgan-specific requirements:** - System prompt must instruct LLM to output valid JSON matching the schema above - For `create_agent_package`, payload must include: `agentId`, `displayName`, `summary`, `tags`, `systemPrompt`, `hints.recommendedIcon`, `hints.whenToUse` - The `systemPrompt` should be wrapped in Web Agent Bundle format ### 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 - `VOICE_INPUT_ENABLED` (default: false) - Future feature for voice input (not yet implemented) ### 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 ## Debugging and Troubleshooting ### Checking Agent Configuration ```bash # View live logs from deployed worker npx wrangler tail # Test agent webhook directly curl -X POST $AGENT_1_URL \ -H "Content-Type: application/json" \ -d '{"message":"test","sessionId":"test-session"}' ``` ### Checking n8n Response Format Look for these log patterns in `npx wrangler tail`: - `[v0] Webhook response body` - Shows raw n8n response - `[v0] Parsed webhook data` - Shows parsed response object - `[v0] parsedOutput messageType` - Shows detected message type (regular_message or tool_call) ### Checking LocalStorage ```javascript // Browser console localStorage.getItem('pinned-agents') // Custom agents array localStorage.getItem('chat-messages-agent-2') // Morgan's chat history localStorage.getItem('selected-agent') // Currently selected agent ``` ### Common Issues **Custom agents not working:** - Verify `CUSTOM_AGENT_WEBHOOK` is set in `wrangler.jsonc` vars - Check browser console for agentId format (`custom-{uuid}`) - Ensure custom agent has systemPrompt in localStorage **Morgan not creating agents:** - Verify Morgan's n8n workflow has Code node for output unwrapping - Check response format matches schema (messageType, content, toolCall) - System prompt must instruct to output valid JSON **Deployment shows "No updated asset files":** - You skipped `npx @opennextjs/cloudflare build` step - Run the OpenNext build before deploying again