- Move route and vars to top level in wrangler.jsonc (remove env.production wrapper) - Update deployment instructions: use 'npx wrangler deploy' without --env flags - Update all documentation (README.md, .cursorrules, CLAUDE.md) to reflect correct deployment process - Route configured as agents.nicholai.work at top level
11 KiB
11 KiB
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
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
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 buildbefore deploying. The standardnext buildalone is insufficient for Cloudflare deployment. - Always use
npx wrangler deploywithout any--envflags. The deployment uses the default configuration fromwrangler.jsoncwith routeagents.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/agentsdynamically 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
- User submits message through
ChatInterfacecomponent - POST
/api/chatreceives message with agentId, sessionId, timestamp, and optional base64 images - Route extracts webhook URL from environment variables based on agentId format (
agent-N) - Message forwarded to n8n webhook with all metadata
- Response parsed from n8n (supports streaming chunks and diff tool calls)
- 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-toolwith JSON payload - n8n can send
type: "tool_call", name: "show_diff"which is converted server-side MarkdownRendererparses diff-tool blocks and renders viaDiffTool→DiffDisplaycomponents- Diff props:
oldCode,newCode,title,language
Code Structure
API Routes
src/app/api/agents/route.ts- Discovers and returns configured agents from environment variablessrc/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 mountingsrc/components/chat-interface.tsx- Full chat UI: message history, input composer, agent dropdown, image attachmentssrc/components/markdown-renderer.tsx- Renders markdown with syntax highlighting and custom diff-tool blockssrc/components/diff-tool.tsx- Wrapper for diff display functionalitysrc/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:
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:
- Streaming chunks (newline-delimited JSON with
type: "item") - Tool calls (
type: "tool_call", name: "show_diff") - Regular JSON with various fields (
response,message,output,text) - 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
varssection - Compatibility flags:
nodejs_compat,global_fetch_strictly_public - Important: Deployment uses default configuration - do not use
--envflags
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-autoto 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.tswith 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 validationDIFF_TOOL_ENABLED(default: true) - Controls diff visualization rendering
Flag Checking Patterns
Server-side (API routes):
import { getFlags } from '@/lib/flags'
const flags = getFlags()
if (!flags.IMAGE_UPLOADS_ENABLED) {
return NextResponse.json({ error: 'Feature disabled' }, { status: 403 })
}
Client-side (Components):
import { useFlags } from '@/lib/use-flags'
const { flags, isLoading, error } = useFlags()
if (flags.IMAGE_UPLOADS_ENABLED) {
// Render feature UI
}
Testing:
import { registerRuntimeFlags, resetFlagsCache } from '@/lib/flags'
beforeEach(() => {
resetFlagsCache()
registerRuntimeFlags({ IMAGE_UPLOADS_ENABLED: false })
})
Configuration
- Development: Set in
.env.local - Production: Set in
wrangler.jsoncunderenv.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 fetchcomponents/- React component rendering testsflags/- Feature flag integration tests
Running Tests
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:
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:
import { render, screen } from '@testing-library/react'
import { MarkdownRenderer } from '@/components/markdown-renderer'
render(<MarkdownRenderer content="# Hello" />)
expect(screen.getByText('Hello')).toBeInTheDocument()
Flag Behavior Testing:
// 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.fetchfor 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