Correspondents/CLAUDE.md

17 KiB
Raw Permalink Blame History

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

    pnpm build
    

    Do not skip this step—its the fastest way to catch type or compile issues before packaging for Cloudflare.

  3. Fix lint errors before deploying:

    pnpm lint
    

    Resolve every reported error (warnings are acceptable) before proceeding so Cloudflare builds dont fail mid-process.

  4. Before every deploy, run the Cloudflare build step:

    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:

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

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

[{
  "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:
    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):

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

Debugging and Troubleshooting

Checking Agent Configuration

# 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

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