Skip to content

Repository files navigation

MailPilot

AI-powered Gmail client where a natural language assistant controls the UI programmatically.

Stack

  • Next.js 14 (App Router), TypeScript, Tailwind CSS
  • Axios, Anthropic Claude Haiku, Gmail API, NextAuth v4

Setup

  1. Copy .env.example to .env.local and fill in values
  2. Enable Gmail API in Google Cloud Console
  3. Create OAuth 2.0 credentials with redirect URI: http://localhost:3000/api/auth/callback/google
  4. Install and run:
npm install
npm run dev
  1. Open http://localhost:3000 and sign in with Google

How it works (HLD)

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
Loading

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

Architecture tradeoffs

Deliberate choices and what we traded for them.

No database, no client data store

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.

Other tradeoffs

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

API Routes

  • POST /api/mail/list — inbox/sent with SFOP filters
  • GET /api/mail/[id] — email thread
  • POST /api/mail/[id]/read — mark thread as read
  • POST /api/mail/send — send email (supports threaded reply)
  • GET /api/mail/search — Gmail search
  • POST /api/assistant — Claude agentic loop
  • GET /api/auth/session-check — validate session / access token
  • /api/auth/[...nextauth] — NextAuth (Google OAuth)

Demo script

Try these prompts in the assistant widget after signing in:

  1. Filter unread: Show me only unread emails
  2. Date range: Show emails from the last 10 days
  3. Find and open: Find the latest email from [name] about [topic] and open it
  4. New compose: Write an email to someone@example.com about rescheduling our meeting
  5. Reply in thread: Open an email, then: Reply saying I'll get back to them tomorrow

Features

  • 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

About

AI-powered Gmail client where a natural language assistant searches, filters, and controls the UI — built with Next.js and Claude.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages