Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e572dbb
improved error handling during attachment downloads. (#1751)
MrgSub Jul 18, 2025
cb0822f
hotfixes (#1753)
MrgSub Jul 18, 2025
3136e86
fix deployment (#1754)
MrgSub Jul 18, 2025
ecb2126
Add auto draft generation (#1645)
ahmetskilinc Jul 18, 2025
6637cb9
fix: prevent browser search from triggering on Ctrl+K (#1757)
ayushsharma74 Jul 18, 2025
e4148d3
refactor durable objects (#1764)
MrgSub Jul 20, 2025
929649d
remove unused ZeroDriver code (#1765)
MrgSub Jul 20, 2025
7d9a6e4
Make agent property nullable in ZeroDriver (#1766)
MrgSub Jul 20, 2025
bf2c8f3
fix: eval lint err for extra arguments and autofix failing test (#1768)
retrogtx Jul 20, 2025
364e90b
Remove console logs and simplify AI chat prompt (#1769)
MrgSub Jul 20, 2025
d040186
Refactor Google mail count implementation and add staleTime to stats …
MrgSub Jul 20, 2025
2e44025
Uncomment and enable automatic draft generation in thread workflow (#…
MrgSub Jul 20, 2025
b0177bc
hotfix stuf (#1775)
MrgSub Jul 21, 2025
4c3753e
feat: ability to snooze emails (#1477)
retrogtx Jul 21, 2025
718b303
fix alignment
nizzyabi Jul 21, 2025
6cc6730
fix deployment (#1779)
MrgSub Jul 21, 2025
2ace344
Remove unused code and optimize database connections (#1778)
MrgSub Jul 21, 2025
b70b880
Add agent documentation and sync additional folders in production (#1…
MrgSub Jul 21, 2025
4caab6b
Implement Sentry error tracking and onboarding email sequence (#1781)
MrgSub Jul 21, 2025
0602b87
feat: update translations via @LingoDotDev (#1776)
github-actions[bot] Jul 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions AGENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Agent Configuration for Zero Email

Zero is an open-source AI email solution built with a modern TypeScript/Next.js stack in a monorepo setup.

## Project Structure

This is a pnpm workspace monorepo with the following structure:
- `apps/mail/` - Next.js frontend email client
- `apps/server/` - Backend server
- `apps/ios-app/` - iOS mobile app
- `packages/cli/` - CLI tools (`nizzy` command)
- `packages/db/` - Database schemas and utilities
- `packages/eslint-config/` - Shared ESLint configuration
- `packages/tailwind-config/` - Shared Tailwind configuration
- `packages/tsconfig/` - Shared TypeScript configuration

## Frequently Used Commands

### Development
- `pnpm go` - Quick start: starts database and dev servers
- `pnpm dev` - Start all development servers (uses Turbo)
- `pnpm docker:db:up` - Start PostgreSQL database in Docker
- `pnpm docker:db:down` - Stop and remove database container
- `pnpm docker:db:clean` - Stop and remove database with volumes

### Build & Deploy
- `pnpm build` - Build all packages (uses Turbo)
- `pnpm build:frontend` - Build only the mail frontend
- `pnpm deploy:frontend` - Deploy frontend
- `pnpm deploy:backend` - Deploy backend

### Code Quality
- `pnpm check` - Run format check and lint
- `pnpm lint` - Run ESLint across all packages
- `pnpm format` - Format code with Prettier
- `pnpm check:format` - Check code formatting

### Database
- `pnpm db:push` - Push schema changes to database
- `pnpm db:generate` - Generate migration files
- `pnpm db:migrate` - Apply database migrations
- `pnpm db:studio` - Open Drizzle Studio

### Testing & Evaluation
- `pnpm test:ai` - Run AI tests
- `pnpm eval` - Run evaluation suite
- `pnpm eval:dev` - Run evaluation in dev mode
- `pnpm eval:ci` - Run evaluation in CI mode

### Utilities
- `pnpm nizzy env` - Setup environment variables
- `pnpm nizzy sync` - Sync environment variables and types
- `pnpm scripts` - Run custom scripts

## Tech Stack

- **Frontend**: Next.js, React 19, TypeScript, TailwindCSS, Shadcn UI
- **Backend**: Node.js, tRPC, Drizzle ORM
- **Database**: PostgreSQL
- **Authentication**: Better Auth, Google OAuth
- **Package Manager**: pnpm (v10+)
- **Build Tool**: Turbo
- **Linting**: ESLint, Oxlint, Prettier

## Code Style & Conventions

### Formatting
- 2-space indentation
- Single quotes
- 100 character line width
- Semicolons required
- Uses Prettier with sort-imports and Tailwind plugins

### File Organization
- TypeScript strict mode enabled
- Workspace packages use catalog versioning for shared dependencies
- Monorepo managed with pnpm workspaces

### Important Environment Variables
- `BETTER_AUTH_SECRET` - Auth secret key
- `GOOGLE_CLIENT_ID` & `GOOGLE_CLIENT_SECRET` - Gmail integration
- `AUTUMN_SECRET_KEY` - Encryption service
- `TWILIO_*` - SMS integration
- `DATABASE_URL` - PostgreSQL connection string

## Development Setup

1. Install dependencies: `pnpm install`
2. Setup environment: `pnpm nizzy env`
3. Sync environment: `pnpm nizzy sync`
4. Start database: `pnpm docker:db:up`
5. Initialize database: `pnpm db:push`
6. Start development: `pnpm dev`

## Common Workflow

1. Always run `pnpm check` before committing
2. Use `pnpm nizzy sync` after environment variable changes
3. Run `pnpm db:push` after schema changes
4. Use `pnpm go` for quick development startup

## Notes

- Uses Husky for git hooks
- Integrates with Sentry for error tracking
- Uses Cloudflare Workers for backend deployment
- iOS app is part of the monorepo
- CLI tool `nizzy` helps manage environment and sync operations
2 changes: 1 addition & 1 deletion apps/mail/app/(routes)/mail/[folder]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { authProxy } from '@/lib/auth-proxy';
import { useEffect, useState } from 'react';
import type { Route } from './+types/page';

const ALLOWED_FOLDERS = new Set(['inbox', 'draft', 'sent', 'spam', 'bin', 'archive']);
const ALLOWED_FOLDERS = new Set(['inbox', 'draft', 'sent', 'spam', 'bin', 'archive', 'snoozed']);

export async function clientLoader({ params, request }: Route.ClientLoaderArgs) {
if (!params.folder) return Response.redirect(`${import.meta.env.VITE_PUBLIC_APP_URL}/mail/inbox`);
Expand Down
5 changes: 2 additions & 3 deletions apps/mail/app/(routes)/settings/notifications/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import {
} from '@/components/ui/select';
import { SettingsCard } from '@/components/settings/settings-card';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { Button } from '@/components/ui/button';
import { useForm } from 'react-hook-form';
import { Bell } from 'lucide-react';
import { useState } from 'react';
Expand All @@ -38,10 +38,9 @@ export default function NotificationsPage() {
},
});

function onSubmit(values: z.infer<typeof formSchema>) {
function onSubmit() {
setIsSaving(true);
setTimeout(() => {
console.log(values);
setIsSaving(false);
}, 1000);
}
Expand Down
41 changes: 21 additions & 20 deletions apps/mail/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,22 @@ import {
type LoaderFunctionArgs,
type MetaFunction,
} from 'react-router';
import { Analytics as DubAnalytics } from '@dub/analytics/react';
import { ServerProviders } from '@/providers/server-providers';
import { ClientProviders } from '@/providers/client-providers';
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import { useEffect, type PropsWithChildren } from 'react';
import { AlertCircle, Loader2 } from 'lucide-react';
import type { AppRouter } from '@zero/server/trpc';
import { Button } from '@/components/ui/button';
import { getLocale } from '@/paraglide/runtime';
import { siteConfig } from '@/lib/site-config';
import { signOut } from '@/lib/auth-client';
import type { Route } from './+types/root';
import { AlertCircle } from 'lucide-react';
import { m } from '@/paraglide/messages';
import { ArrowLeft } from 'lucide-react';
import superjson from 'superjson';
import './globals.css';
import { Analytics as DubAnalytics } from '@dub/analytics/react';


const getUrl = () => import.meta.env.VITE_PUBLIC_BACKEND_URL + '/api/trpc';

Expand Down Expand Up @@ -55,13 +54,13 @@ export const meta: MetaFunction = () => {
];
};

export async function loader({ request }: LoaderFunctionArgs) {
const trpc = getServerTrpc(request);
const defaultConnection = await trpc.connections.getDefault
.query()
.then((res) => (res?.id as string) ?? null)
.catch(() => null);
return { connectionId: defaultConnection };
export async function loader(_: LoaderFunctionArgs) {
// const trpc = getServerTrpc(request);
// const defaultConnection = await trpc.connections.getDefault
// .query()
// .then((res) => (res?.id as string) ?? null)
// .catch(() => null);
return { connectionId: 'defaultConnection' };
}

export function Layout({ children }: PropsWithChildren) {
Expand All @@ -84,9 +83,11 @@ export function Layout({ children }: PropsWithChildren) {
<body className="antialiased">
<ServerProviders connectionId={connectionId}>
<ClientProviders>{children}</ClientProviders>
<DubAnalytics domainsConfig={{
refer: "mail0.com"
}} />
<DubAnalytics
domainsConfig={{
refer: 'mail0.com',
}}
/>
</ServerProviders>
<ScrollRestoration />
<Scripts />
Expand All @@ -95,13 +96,13 @@ export function Layout({ children }: PropsWithChildren) {
);
}

export function HydrateFallback() {
return (
<div className="flex h-screen w-full items-center justify-center">
<Loader2 className="h-10 w-10 animate-spin" />
</div>
);
}
// export function HydrateFallback() {
// return (
// <div className="flex h-screen w-full items-center justify-center">
// <Loader2 className="h-10 w-10 animate-spin" />
// </div>
// );
// }

export default function App() {
return <Outlet />;
Expand Down
6 changes: 3 additions & 3 deletions apps/mail/components/context/command-palette-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -278,8 +278,8 @@ export function CommandPalette({ children }: { children: React.ReactNode }) {
}
};

document.addEventListener('keydown', down);
return () => document.removeEventListener('keydown', down);
document.addEventListener('keydown', down, { capture: true });
return () => document.removeEventListener('keydown', down, { capture: true });
}, [open, currentView]);

const runCommand = useCallback((command: () => unknown) => {
Expand Down Expand Up @@ -739,7 +739,7 @@ export function CommandPalette({ children }: { children: React.ReactNode }) {

const result: CommandGroup[] = [
{
group: 'Search & Filter',
group: 'Search',
items: searchCommands,
},
{
Expand Down
Loading