This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
21st Agents - A local-first Electron desktop app for AI-powered code assistance. Users create chat sessions linked to local project folders, interact with Claude in Plan or Agent mode, and see real-time tool execution (bash, file edits, web search, etc.).
# Development
bun run dev # Start Electron with hot reload
# Build
bun run build # Compile app
bun run package # Package for current platform (dir)
bun run package:mac # Build macOS (DMG + ZIP)
bun run package:win # Build Windows (NSIS + portable)
bun run package:linux # Build Linux (AppImage + DEB)
# Database (Drizzle + SQLite)
bun run db:generate # Generate migrations from schema
bun run db:push # Push schema directly (dev only)src/
├── main/ # Electron main process
│ ├── index.ts # App entry, window lifecycle
│ ├── auth-manager.ts # OAuth flow, token refresh
│ ├── auth-store.ts # Encrypted credential storage (safeStorage)
│ ├── windows/main.ts # Window creation, IPC handlers
│ └── lib/
│ ├── db/ # Drizzle + SQLite
│ │ ├── index.ts # DB init, auto-migrate on startup
│ │ ├── schema/ # Drizzle table definitions
│ │ └── utils.ts # ID generation
│ └── trpc/routers/ # tRPC routers (projects, chats, claude)
│
├── preload/ # IPC bridge (context isolation)
│ └── index.ts # Exposes desktopApi + tRPC bridge
│
└── renderer/ # React 19 UI
├── App.tsx # Root with providers
├── features/
│ ├── agents/ # Main chat interface
│ │ ├── main/ # active-chat.tsx, new-chat-form.tsx
│ │ ├── ui/ # Tool renderers, preview, diff view
│ │ ├── commands/ # Slash commands (/plan, /agent, /clear)
│ │ ├── atoms/ # Jotai atoms for agent state
│ │ └── stores/ # Zustand store for sub-chats
│ ├── sidebar/ # Chat list, archive, navigation
│ ├── sub-chats/ # Tab/sidebar sub-chat management
│ └── layout/ # Main layout with resizable panels
├── components/ui/ # Radix UI wrappers (button, dialog, etc.)
└── lib/
├── atoms/ # Global Jotai atoms
├── stores/ # Global Zustand stores
├── trpc.ts # Real tRPC client
└── mock-api.ts # DEPRECATED - being replaced with real tRPC
Location: {userData}/data/agents.db (SQLite)
Schema: src/main/lib/db/schema/index.ts
// Three main tables:
projects → id, name, path (local folder), timestamps
chats → id, name, projectId, worktree fields, timestamps
sub_chats → id, name, chatId, sessionId, mode, messages (JSON)Auto-migration: On app start, initDatabase() runs migrations from drizzle/ folder (dev) or resources/migrations (packaged).
Queries:
import { getDatabase, projects, chats } from "../lib/db"
import { eq } from "drizzle-orm"
const db = getDatabase()
const allProjects = db.select().from(projects).all()
const projectChats = db.select().from(chats).where(eq(chats.projectId, id)).all()- Uses tRPC with
trpc-electronfor type-safe main↔renderer communication - All backend calls go through tRPC routers, not raw IPC
- Preload exposes
window.desktopApifor native features (window controls, clipboard, notifications)
- Jotai: UI state (selected chat, sidebar open, preview settings)
- Zustand: Sub-chat tabs and pinned state (persisted to localStorage)
- React Query: Server state via tRPC (auto-caching, refetch)
- Dynamic import of
@anthropic-ai/claude-codeSDK - Two modes: "plan" (read-only) and "agent" (full permissions)
- Session resume via
sessionIdstored in SubChat - Message streaming via tRPC subscription (
claude.onMessage)
| Layer | Tech |
|---|---|
| Desktop | Electron 33.4.5, electron-vite, electron-builder |
| UI | React 19, TypeScript 5.4.5, Tailwind CSS |
| Components | Radix UI, Lucide icons, Motion, Sonner |
| State | Jotai, Zustand, React Query |
| Backend | tRPC, Drizzle ORM, better-sqlite3 |
| AI | @anthropic-ai/claude-code |
| Package Manager | bun |
- Components: PascalCase (
ActiveChat.tsx,AgentsSidebar.tsx) - Utilities/hooks: camelCase (
useFileUpload.ts,formatters.ts) - Stores: kebab-case (
sub-chat-store.ts,agent-chat-store.ts) - Atoms: camelCase with
Atomsuffix (selectedAgentChatIdAtom)
electron.vite.config.ts- Build config (main/preload/renderer entries)src/main/lib/db/schema/index.ts- Drizzle schema (source of truth)src/main/lib/db/index.ts- DB initialization + auto-migratesrc/renderer/features/agents/atoms/index.ts- Agent UI state atomssrc/renderer/features/agents/main/active-chat.tsx- Main chat componentsrc/main/lib/trpc/routers/claude.ts- Claude SDK integration
When testing auth flows or behavior for new users, you need to simulate a fresh install:
# 1. Clear all app data (auth, database, settings)
rm -rf ~/Library/Application\ Support/Agents\ Dev/
# 2. Reset macOS protocol handler registration (if testing deep links)
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -kill -r -domain local -domain system -domain user
# 3. Clear app preferences
defaults delete dev.21st.agents.dev # Dev mode
defaults delete dev.21st.agents # Production
# 4. Run in dev mode with clean state
cd apps/desktop
bun run devCommon First-Install Bugs:
- OAuth deep link not working: macOS Launch Services may not immediately recognize protocol handlers on first app launch. User may need to click "Sign in" again after the first attempt.
- Folder dialog not appearing: Window focus timing issues on first launch. Fixed by ensuring window focus before showing
dialog.showOpenDialog().
Dev vs Production App:
- Dev mode uses
twentyfirst-agents-dev://protocol - Dev mode uses separate userData path (
~/Library/Application Support/Agents Dev/) - This prevents conflicts between dev and production installs
Set environment variables (add to .zshrc or .env.local):
export APPLE_ID="your-apple-id@example.com"
export APPLE_APP_SPECIFIC_PASSWORD="xxxx-xxxx-xxxx-xxxx" # App-specific password from appleid.apple.com# Full release (build, sign, notarize, generate manifests, upload to CDN)
bun run release
# Or step by step:
bun run build # Compile TypeScript
bun run package:mac # Build, sign & notarize macOS app
bun run dist:manifest # Generate latest-mac.yml manifests
./scripts/upload-release-wrangler.sh # Upload to R2 CDNnpm version patch # 0.0.2 → 0.0.3
npm version minor # 0.0.3 → 0.1.0
npm version major # 0.1.0 → 1.0.0| File | Purpose |
|---|---|
latest-mac.yml |
Manifest for arm64 auto-updates |
latest-mac-x64.yml |
Manifest for Intel auto-updates |
Agents-{version}-arm64-mac.zip |
Auto-update payload (arm64) |
Agents-{version}-mac.zip |
Auto-update payload (Intel) |
Agents-{version}-arm64.dmg |
Manual download (arm64) |
Agents-{version}.dmg |
Manual download (Intel) |
- App checks
https://cdn.21st.dev/releases/desktop/latest-mac.ymlon startup and when window regains focus (with 1 min cooldown) - If version in manifest > current version, shows "Update Available" banner
- User clicks Download → downloads ZIP in background
- User clicks "Restart Now" → installs update and restarts
Done:
- Drizzle ORM setup with schema (projects, chats, sub_chats)
- Auto-migration on app startup
- tRPC routers structure
In Progress:
- Replacing
mock-api.tswith real tRPC calls in renderer - ProjectSelector component (local folder picker)
Planned:
- Git worktree per chat (isolation)
- Claude Code execution in worktree path
- Full feature parity with web app