Correspondents/CLAUDE.md

9.8 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 deploy --env=""       # Deploy to default environment explicitly
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.

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 DiffToolDiffDisplay 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:

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
  • Production route: agents.nicholai.work
  • Compatibility flags: nodejs_compat, global_fetch_strictly_public

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

Message Display

  • User messages: right-aligned with glass bubble styling
  • Assistant messages: markdown-rendered with copy button and syntax highlighting
  • Error messages: red text, optional hint field displayed below
  • Loading state: animated shimmer bar

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):

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

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