AI-powered Gmail client where a natural language assistant controls the UI programmatically.
- Next.js 14 (App Router), TypeScript, Tailwind CSS
- Axios, Anthropic Claude Haiku, Gmail API, NextAuth v4
- Copy
.env.exampleto.env.localand fill in values - Enable Gmail API in Google Cloud Console
- Create OAuth 2.0 credentials with redirect URI:
http://localhost:3000/api/auth/callback/google - Install and run:
npm install
npm run dev- Open http://localhost:3000 and sign in with Google
flowchart LR
User --> AssistantWidget
AssistantWidget -->|"POST /api/assistant"| AgentLoop
AgentLoop -->|"search_emails"| Gmail
AgentLoop -->|"uiActions"| EventBus
EventBus --> AssistantUIProvider
AssistantUIProvider --> Router
AssistantUIProvider --> ComposeHandoff
EmailFilters --> Kernel
Kernel -->|"POST /api/mail/list"| Gmail
Core principle: The AI emits typed intent (uiActions). The UI reacts via a client-side event bus. The AI never touches the DOM.
| Layer | Role |
|---|---|
assistant/ |
Chat widget; POSTs message + context + history to API |
layout/AssistantUIContext |
Subscribes to bus; routes navigation; compose/reply handoff refs |
EmailFilters/ |
Filter state + URL sync |
email-kernel/ |
List fetch + cursor pagination (filters consumed from parent) |
mail/List |
Inbox/sent composer |
mail/Detail |
Thread view + inline reply |
compose/ |
New email only |
lib/assistant-bus |
Client-side pub/sub for uiActions (EventBus in diagram) |
modules/mail |
Gmail adapter, SFOP translation, server services |
modules/assistant |
Claude agentic loop + tools |
Deliberate choices and what we traded for them.
MailPilot is intentionally stateless at both layers:
Server: No database. Gmail is the source of truth; OAuth tokens live in NextAuth JWT sessions (no DB adapter). Mail and assistant routes proxy to Gmail/Claude on each request — nothing is persisted to disk.
Client: No TanStack Query, SWR, Zustand, or Redux. Lists and threads fetch into local React state; filters live in the URL. Assistant chat history exists only in the widget's component state (lost on refresh).
Why: Keeps the app simple for a Gmail shell — no schema, migrations, sync jobs, or cache invalidation.
Tradeoff: No offline support, no chat persistence, and refetch-on-navigation instead of instant cached views.
| Choice | Why | Tradeoff |
|---|---|---|
Typed uiActions + client event bus |
Decouples AI from DOM; user sees real UI | More wiring than a chat-only assistant |
| UI actions execute on the client | Navigation and compose handoff are client concerns | Extra round-trip before UI updates |
filter_inbox default, search_emails internal |
Inbox is the source of truth, not chat | Strict tool/prompt rules for the model |
| No search + UI action in same turn | Prevents reply/open on hallucinated message IDs | Extra agent turns for find-then-act flows |
| Minimal context (IDs + filters only) | Lower token cost, less stale/hallucinated context | Model must search or use the open thread |
| URL-synced filters | Shareable state; assistant and human use the same path | URL serialization complexity |
| Ref-based compose/reply handoff | Email bodies don't belong in URLs | Drafts lost on full page refresh |
| No send tool | Human confirms irreversible actions | No full "send this now" automation |
| SFOP over raw Gmail queries | Provider-agnostic list API | Translation layer overhead for one provider |
| Claude Haiku + 8-message history | Fast, cheap, intent-focused turns | Weaker on long ambiguous threads |
POST /api/mail/list— inbox/sent with SFOP filtersGET /api/mail/[id]— email threadPOST /api/mail/[id]/read— mark thread as readPOST /api/mail/send— send email (supports threaded reply)GET /api/mail/search— Gmail searchPOST /api/assistant— Claude agentic loopGET /api/auth/session-check— validate session / access token/api/auth/[...nextauth]— NextAuth (Google OAuth)
Try these prompts in the assistant widget after signing in:
- Filter unread:
Show me only unread emails - Date range:
Show emails from the last 10 days - Find and open:
Find the latest email from [name] about [topic] and open it - New compose:
Write an email to someone@example.com about rescheduling our meeting - Reply in thread: Open an email, then:
Reply saying I'll get back to them tomorrow
- Assistant composes new mail and replies in-thread via natural language
- Assistant searches/filters and updates the main UI
- Inbox and Sent views with real Gmail data
- Context-aware assistant (view, filters, open email)
- Human-in-the-loop: assistant never sends — user clicks Send