Correspondents/CLAUDE.md

457 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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—its 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 dont 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(<MarkdownRenderer content="# Hello" />)
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