Skip to content

Conversation

nizzyabi
Copy link
Collaborator

@nizzyabi nizzyabi commented Aug 22, 2025

READ CAREFULLY THEN REMOVE

Remove bullet points that are not relevant.

PLEASE REFRAIN FROM USING AI TO WRITE YOUR CODE AND PR DESCRIPTION. IF YOU DO USE AI TO WRITE YOUR CODE PLEASE PROVIDE A DESCRIPTION AND REVIEW IT CAREFULLY. MAKE SURE YOU UNDERSTAND THE CODE YOU ARE SUBMITTING USING AI.

  • Pull requests that do not follow these guidelines will be closed without review or comment.
  • If you use AI to write your PR description your pr will be close without review or comment.
  • If you are unsure about anything, feel free to ask for clarification.

Description

Please provide a clear description of your changes.


Type of Change

Please delete options that are not relevant.

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature with breaking changes)
  • 📝 Documentation update
  • 🎨 UI/UX improvement
  • 🔒 Security enhancement
  • ⚡ Performance improvement

Areas Affected

Please check all that apply:

  • Email Integration (Gmail, IMAP, etc.)
  • User Interface/Experience
  • Authentication/Authorization
  • Data Storage/Management
  • API Endpoints
  • Documentation
  • Testing Infrastructure
  • Development Workflow
  • Deployment/Infrastructure

Testing Done

Describe the tests you've done:

  • Unit tests added/updated
  • Integration tests added/updated
  • Manual testing performed
  • Cross-browser testing (if UI changes)
  • Mobile responsiveness verified (if UI changes)

Security Considerations

For changes involving data or authentication:

  • No sensitive data is exposed
  • Authentication checks are in place
  • Input validation is implemented
  • Rate limiting is considered (if applicable)

Checklist

  • I have read the CONTRIBUTING document
  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in complex areas
  • I have updated the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix/feature works
  • All tests pass locally
  • Any dependent changes are merged and published

Additional Notes

Add any other context about the pull request here.

Screenshots/Recordings

Add screenshots or recordings here if applicable.


By submitting this pull request, I confirm that my contribution is made under the terms of the project's license.


Summary by cubic

Add Founder Mode: a focused triage view to quickly process unread inbox threads with generated draft replies, fast send + archive, and handy shortcuts. This helps reach inbox zero faster.

  • New Features
    • New route at /founder-mode and sidebar item (Zap icon, shortcut g + f).
    • Loads up to 100 unread Inbox threads and refreshes every minute.
    • Skips drafts and messages from yourself; uses the latest message per thread.
    • Creates an editable draft reply with a regenerate option and time received context.
    • One-click Send & Archive or Archive only; marks as read, removes from Inbox, and prevents reprocessing via session cache.
    • Keyboard shortcuts: Tab = generate new draft, Cmd/Ctrl+Enter = send & archive, Cmd/Ctrl+Delete = archive, Arrow keys = navigate.
    • Clear empty states and errors: no connection prompt, inbox zero screen, and a safe fallback draft if generation fails.

Summary by CodeRabbit

  • New Features
    • Added Founder Mode to streamline triage of unread emails.
    • Auto-loads unread threads, lets you navigate between messages, and skips already archived ones.
    • One-click AI draft generation; edit, regenerate, then Send & Archive or Archive only.
    • Keyboard shortcuts: Tab (generate), Cmd/Ctrl+Enter (send & archive), Cmd/Ctrl+Delete (archive), Arrow keys (navigate).
    • Displays sender, subject, content, and relative received time with clear loading and “Inbox Zero” states.
    • New sidebar link to Founder Mode with shortcut g then f.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 22, 2025

Walkthrough

Adds a new Founder Mode page that fetches unread inbox threads, generates AI draft replies, lets users edit/send, archives threads, and navigates emails with keyboard shortcuts. Integrates route configuration and navigation entry with icon and shortcut. Persists processed/archived state in sessionStorage and uses TRPC queries/mutations for mail and AI.

Changes

Cohort / File(s) Summary
Founder Mode Page
apps/mail/app/(routes)/founder-mode/page.tsx
New client-side page component to triage unread threads: fetch unread, filter archived via sessionStorage, sequentially process threads, generate AI replies (TRPC), allow edits, send & archive or archive-only, mark-as-read, invalidate/refetch queries, keyboard shortcuts, and UI states.
Routing
apps/mail/app/routes.ts
Adds route entry '/founder-mode' mapped to '(routes)/founder-mode/page.tsx' within the public layout.
Navigation
apps/mail/config/navigation.ts
Adds “Founder Mode” nav item with Zap icon and shortcut g + f; imports Zap from lucide-react.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant FM as FounderMode Page
  participant MAIL as TRPC Mail API
  participant AI as TRPC AI Compose
  participant SS as sessionStorage

  U->>FM: Navigate to /founder-mode
  FM->>MAIL: getUnreadThreads(INBOX)
  MAIL-->>FM: unread thread list
  FM->>SS: read founderMode_archivedIds
  FM-->>FM: filter out archived/processed
  loop For each thread
    FM->>MAIL: getThread(threadId)
    MAIL-->>FM: thread data
    FM-->>FM: extract latest incoming message
    U->>FM: Press Tab (generate)
    FM->>AI: composeReply(context)
    alt success
      AI-->>FM: generated reply
      FM-->>FM: set generatedReply, editedReply
    else failure
      AI-->>FM: error
      FM-->>FM: set fallback reply
    end
    U->>FM: Edit reply (optional)
    alt Send & Archive (Cmd/Ctrl+Enter)
      FM->>MAIL: sendReply(threadId, editedReply)
      MAIL-->>FM: ok
      FM->>MAIL: bulkArchive([threadId])
      MAIL-->>FM: ok
      FM->>MAIL: markAsRead([threadId])
      MAIL-->>FM: ok
      FM->>SS: update archivedIds
      FM->>MAIL: invalidate+refetch unread
      FM-->>FM: advance to next email
    else Archive only (Cmd/Ctrl+Delete)
      FM->>MAIL: bulkArchive([threadId])
      MAIL-->>FM: ok
      FM->>SS: update archivedIds
      FM->>MAIL: invalidate+refetch unread
      FM-->>FM: advance to next email
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

high priority

Suggested reviewers

  • ahmetskilinc
  • MrgSub

Poem

New mode engaged, laser focus on the load,
Unread threads align — we triage, auto-road.
AI drafts the pitch, humans add the soul,
Send, archive, next — achieving inbox control.
Zap to founder flow, keyboard rockets ignite,
Inbox zero orbit — clean skies, clear night. 🚀

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch founder

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai bot requested review from MrgSub and ahmetskilinc August 22, 2025 16:02
@coderabbitai coderabbitai bot added the High Priority High Priority Work label Aug 22, 2025
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 3 files

React with 👍 or 👎 to teach cubic. You can also tag @cubic-dev-ai to give feedback, ask questions, or re-run the review.

},
{
id: 'founder-mode',
title: 'Founder Mode',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a localized message key instead of a hardcoded string for the title to comply with the file's i18n requirement.

(Based on your team's feedback about keeping navigation titles sourced from message keys for localization consistency.)

Prompt for AI agents
Address the following comment on apps/mail/config/navigation.ts at line 61:

<comment>Use a localized message key instead of a hardcoded string for the title to comply with the file&#39;s i18n requirement.

(Based on your team&#39;s feedback about keeping navigation titles sourced from message keys for localization consistency.)</comment>

<file context>
@@ -56,6 +56,13 @@ export const navigationConfig: Record&lt;string, NavConfig&gt; = {
             icon: Inbox,
             shortcut: &#39;g + i&#39;,
           },
+          {
+            id: &#39;founder-mode&#39;,
+            title: &#39;Founder Mode&#39;,
+            url: &#39;/founder-mode&#39;,
+            icon: Zap,
</file context>
Suggested change
title: 'Founder Mode',
title: m['navigation.sidebar.founderMode'](),

variant="ghost"
onClick={regenerateReply}
disabled={currentEmail.isGenerating}
title="Generate new reply (Shift+Tab)"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tooltip claims Shift+Tab but the implemented shortcut is Tab; update the label for accuracy.

Prompt for AI agents
Address the following comment on apps/mail/app/(routes)/founder-mode/page.tsx at line 585:

<comment>Tooltip claims Shift+Tab but the implemented shortcut is Tab; update the label for accuracy.</comment>

<file context>
@@ -0,0 +1,657 @@
+&#39;use client&#39;;
+
+import { useCallback, useEffect, useMemo, useState } from &#39;react&#39;;
+import { useActiveConnection } from &#39;@/hooks/use-connections&#39;;
+import { useTRPC } from &#39;@/providers/query-provider&#39;;
+import { useQuery, useMutation, useQueryClient } from &#39;@tanstack/react-query&#39;;
+import { Button } from &#39;@/components/ui/button&#39;;
+import { Loader2, X, Check, RefreshCw } from &#39;lucide-react&#39;;
+import { useNavigate } from &#39;react-router&#39;;
</file context>


// Use sessionStorage to persist archived IDs across refreshes
const [archivedIds] = useState<Set<string>>(() => {
const stored = sessionStorage.getItem('founderMode_archivedIds');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct sessionStorage access during render can crash on SSR; guard with window check in the initializer.

Prompt for AI agents
Address the following comment on apps/mail/app/(routes)/founder-mode/page.tsx at line 37:

<comment>Direct sessionStorage access during render can crash on SSR; guard with window check in the initializer.</comment>

<file context>
@@ -0,0 +1,657 @@
+&#39;use client&#39;;
+
+import { useCallback, useEffect, useMemo, useState } from &#39;react&#39;;
+import { useActiveConnection } from &#39;@/hooks/use-connections&#39;;
+import { useTRPC } from &#39;@/providers/query-provider&#39;;
+import { useQuery, useMutation, useQueryClient } from &#39;@tanstack/react-query&#39;;
+import { Button } from &#39;@/components/ui/button&#39;;
+import { Loader2, X, Check, RefreshCw } from &#39;lucide-react&#39;;
+import { useNavigate } from &#39;react-router&#39;;
</file context>

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/mail/config/navigation.ts (1)

46-66: Let’s sync Founder Mode’s route with the Mail section

We spotted that active-state highlighting is driven by:

  • in app-sidebar.tsx, location.pathname.startsWith(config.path) (config.path is '/mail')
  • in nav-main.tsx, isUrlActive(item.url) (matches prefix of item.url)

Because the Founder Mode item currently points at '/founder-mode', users landing on /mail/founder-mode won’t see it highlighted. To fix this, please:

• In apps/mail/config/navigation.ts, change the Founder Mode URL
• In apps/mail/app/routes.ts, update its route declaration to '/mail/founder-mode' so it inherits the Mail layout and matches config.path
• Confirm in app-sidebar.tsx (lines ~48-50) and nav-main.tsx (lines ~227-230) that active logic now correctly flags Founder Mode under /mail

Proposed diff for navigation.ts:

           {
             id: 'founder-mode',
-            url: '/founder-mode',
+            url: '/mail/founder-mode',
             title: m['navigation.sidebar.founderMode'](),
             icon: Zap,
             shortcut: 'g + f',
           },

Let’s nail this so our sidebar behaves predictably. 🚀

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e67c76a and 08a36de.

📒 Files selected for processing (3)
  • apps/mail/app/(routes)/founder-mode/page.tsx (1 hunks)
  • apps/mail/app/routes.ts (1 hunks)
  • apps/mail/config/navigation.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{css,js,ts,jsx,tsx,mdx}

📄 CodeRabbit inference engine (.cursor/rules/tailwind-css-v4.mdc)

**/*.{css,js,ts,jsx,tsx,mdx}: Chain variants together for composable variants (e.g., group-has-data-potato:opacity-100).
Use new variants such as starting, not-*, inert, nth-*, in-*, open (for :popover-open), and ** for all descendants.
Do not use deprecated utilities like bg-opacity-*, text-opacity-*, border-opacity-*, and divide-opacity-*; use the new syntax (e.g., bg-black/50).
Use renamed utilities: shadow-sm is now shadow-xs, shadow is now shadow-sm, drop-shadow-sm is now drop-shadow-xs, drop-shadow is now drop-shadow-sm, blur-sm is now blur-xs, blur is now blur-sm, rounded-sm is now rounded-xs, rounded is now rounded-sm, outline-none is now outline-hidden.
Use bg-(--brand-color) syntax for CSS variables in arbitrary values instead of bg-[--brand-color].
Stacked variants now apply left-to-right instead of right-to-left.

Files:

  • apps/mail/config/navigation.ts
  • apps/mail/app/(routes)/founder-mode/page.tsx
  • apps/mail/app/routes.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (AGENT.md)

**/*.{js,jsx,ts,tsx}: Use 2-space indentation
Use single quotes
Limit lines to 100 characters
Semicolons are required

Files:

  • apps/mail/config/navigation.ts
  • apps/mail/app/(routes)/founder-mode/page.tsx
  • apps/mail/app/routes.ts
**/*.{js,jsx,ts,tsx,css}

📄 CodeRabbit inference engine (AGENT.md)

Use Prettier with sort-imports and Tailwind plugins

Files:

  • apps/mail/config/navigation.ts
  • apps/mail/app/(routes)/founder-mode/page.tsx
  • apps/mail/app/routes.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENT.md)

Enable TypeScript strict mode

Files:

  • apps/mail/config/navigation.ts
  • apps/mail/app/(routes)/founder-mode/page.tsx
  • apps/mail/app/routes.ts
🧬 Code graph analysis (1)
apps/mail/app/(routes)/founder-mode/page.tsx (3)
apps/mail/hooks/use-undo-send.ts (1)
  • EmailData (8-17)
apps/mail/hooks/use-connections.ts (1)
  • useActiveConnection (10-22)
apps/server/src/routes/agent/index.ts (1)
  • bulkArchive (834-842)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: cubic · AI code reviewer
🔇 Additional comments (3)
apps/mail/config/navigation.ts (1)

18-18: Icon import looks good and consistent with existing usage.

Importing Zap from lucide-react is aligned with how MessageSquareIcon is already consumed here. No issues.

apps/mail/app/(routes)/founder-mode/page.tsx (2)

9-9: Imports are solid — useNavigate is consistently from ‘react-router’

Verified across every TS/TSX file: every useNavigate import comes from 'react-router', with zero occurrences of 'react-router-dom'. You’re all set—no changes needed.

Likely an incorrect or invalid review comment.


256-264: bulkArchive & markAsRead payload shape confirmed – no action required

The TRPC router definitions in apps/server/src/trpc/routes/mail.ts use:

.input(
  z.object({
    ids: z.string().array(),

for both bulkArchive and markAsRead, so your client calls passing { ids: [...] } are already correct.

Comment on lines +13 to +23
interface EmailData {
threadId: string;
subject: string;
sender: string;
senderEmail: string;
content: string;
receivedOn: string;
generatedReply: string;
editedReply: string;
isGenerating: boolean;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Naming clash with a global EmailData type in hooks/use-undo-send.ts; pick a unique name.

This local interface diverges from the shared EmailData type. Rename to FounderEmail or TriageEmail to avoid confusion for readers and tooling.

Apply this diff:

-interface EmailData {
+interface TriageEmail {
   threadId: string;
   subject: string;
   sender: string;
   senderEmail: string;
   content: string;
   receivedOn: string;
   generatedReply: string;
   editedReply: string;
   isGenerating: boolean;
 }
@@
-  const [emails, setEmails] = useState<EmailData[]>([]);
+  const [emails, setEmails] = useState<TriageEmail[]>([]);
@@
-    const emailData: EmailData = {
+    const emailData: TriageEmail = {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
interface EmailData {
threadId: string;
subject: string;
sender: string;
senderEmail: string;
content: string;
receivedOn: string;
generatedReply: string;
editedReply: string;
isGenerating: boolean;
}
// Rename the interface to avoid clashing with the global EmailData type
interface TriageEmail {
threadId: string;
subject: string;
sender: string;
senderEmail: string;
content: string;
receivedOn: string;
generatedReply: string;
editedReply: string;
isGenerating: boolean;
}
// ...
// Update the state hook to use the new interface name
const [emails, setEmails] = useState<TriageEmail[]>([]);
// ...
// Use the new interface when creating a fresh email object
const emailData: TriageEmail = {
threadId,
subject,
sender,
senderEmail,
content,
receivedOn,
generatedReply,
editedReply,
isGenerating,
};
🤖 Prompt for AI Agents
In apps/mail/app/(routes)/founder-mode/page.tsx around lines 13 to 23, the local
interface EmailData conflicts with a global EmailData type referenced by
hooks/use-undo-send.ts; rename the interface to a unique name (e.g.,
FounderEmail or TriageEmail) and update every usage in this file to the new name
to avoid clashes with global types and tooling confusion.

Comment on lines +35 to +41
// Use sessionStorage to persist archived IDs across refreshes
const [archivedIds] = useState<Set<string>>(() => {
const stored = sessionStorage.getItem('founderMode_archivedIds');
return stored ? new Set(JSON.parse(stored)) : new Set();
});
const trpc = useTRPC();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

State anti-pattern: mutating a Set in state prevents reactive updates (threads won’t filter correctly).

You mutate archivedIds directly (Set#add) but never change its identity. Effects and memos depending on archivedIds won’t re-run, leading to re-processing already archived threads.

Apply this diff to make archivedIds reactive and persisted:

-  // Use sessionStorage to persist archived IDs across refreshes
-  const [archivedIds] = useState<Set<string>>(() => {
-    const stored = sessionStorage.getItem('founderMode_archivedIds');
-    return stored ? new Set(JSON.parse(stored)) : new Set();
-  });
+  // Use sessionStorage to persist archived IDs across refreshes
+  const [archivedIds, setArchivedIds] = useState<Set<string>>(() => {
+    const stored = sessionStorage.getItem('founderMode_archivedIds');
+    return stored ? new Set(JSON.parse(stored)) : new Set();
+  });
+  useEffect(() => {
+    // Persist whenever the set identity changes
+    sessionStorage.setItem('founderMode_archivedIds', JSON.stringify(Array.from(archivedIds)));
+  }, [archivedIds]);

And update mutation handlers to use the setter (see diffs below).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Use sessionStorage to persist archived IDs across refreshes
const [archivedIds] = useState<Set<string>>(() => {
const stored = sessionStorage.getItem('founderMode_archivedIds');
return stored ? new Set(JSON.parse(stored)) : new Set();
});
const trpc = useTRPC();
// Use sessionStorage to persist archived IDs across refreshes
const [archivedIds, setArchivedIds] = useState<Set<string>>(() => {
const stored = sessionStorage.getItem('founderMode_archivedIds');
return stored ? new Set(JSON.parse(stored)) : new Set();
});
useEffect(() => {
// Persist whenever the set identity changes
sessionStorage.setItem(
'founderMode_archivedIds',
JSON.stringify(Array.from(archivedIds))
);
}, [archivedIds]);
const trpc = useTRPC();
🤖 Prompt for AI Agents
In apps/mail/app/(routes)/founder-mode/page.tsx around lines 35–41, archivedIds
is created as a Set but mutated in-place which prevents React from detecting
changes; change useState to capture both value and setter (e.g., const
[archivedIds, setArchivedIds] = useState<Set<string>>(...)), never mutate the
Set in-place — when archiving/unarchiving produce a new Set (copy existing into
new Set or use new Set([...old, id]) / new Set([...old].filter(...))) and call
setArchivedIds(newSet); add a useEffect that serializes archivedIds to
sessionStorage whenever archivedIds changes; update any mutation handlers and
memos/effects to use setArchivedIds instead of mutating archivedIds so dependent
effects re-run properly.

Comment on lines +242 to +306
// Send reply and archive
const sendReplyAndArchive = useCallback(async () => {
if (!currentEmail || !currentEmail.editedReply || currentEmail.isGenerating || isProcessing) return;

setIsProcessing(true);

try {
// Convert plain text with newlines to HTML format
const formattedMessage = currentEmail.editedReply
.split('\n')
.map(line => line.trim() ? `<p>${line}</p>` : '<br/>')
.join('');

// Send the reply
await sendEmail.mutateAsync({
to: [{ email: currentEmail.senderEmail }],
subject: currentEmail.subject.startsWith('Re:')
? currentEmail.subject
: `Re: ${currentEmail.subject}`,
message: formattedMessage,
threadId: currentEmail.threadId,
});

// Use bulkArchive which marks as read AND removes from inbox
await bulkArchive.mutateAsync({
ids: [currentEmail.threadId],
});

// Also explicitly mark as read to be sure
await markAsRead.mutateAsync({
ids: [currentEmail.threadId],
});

// Add to archived set to prevent re-fetching
archivedIds.add(currentEmail.threadId);
sessionStorage.setItem('founderMode_archivedIds', JSON.stringify(Array.from(archivedIds)));

// Also add to processed IDs to prevent re-processing
setProcessedThreadIds(prev => new Set([...prev, currentEmail.threadId]));

// Remove from list
setEmails(prev => prev.filter(e => e.threadId !== currentEmail.threadId));

// Reset index if needed
if (emails.length <= 1) {
setCurrentIndex(0);
} else if (currentIndex >= emails.length - 1) {
setCurrentIndex(Math.max(0, currentIndex - 1));
}

// Invalidate queries to refresh data
await queryClient.invalidateQueries({ queryKey: ['mail.listThreads'] });
await queryClient.invalidateQueries({ queryKey: ['mail.get', currentEmail.threadId] });
await queryClient.invalidateQueries({ queryKey: ['useThreads'] });

// Force refetch to update the unread list
refetch();

} catch (error) {
console.error('Failed to send/archive:', error);
console.error('Failed to send email. Please try again.');
} finally {
setIsProcessing(false);
}
}, [currentEmail, sendEmail, markAsRead, bulkArchive, currentIndex, emails.length, isProcessing, queryClient, archivedIds, refetch]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

DRY up duplicate post-action flows (invalidate, remove, index math).

sendReplyAndArchive and archiveOnly share a long tail of identical steps. Extract a helper to reduce surface area for bugs, especially around index math and query invalidations.

Here’s a minimal, localized refactor:

+  const finalizeThreadRemoval = useCallback(async (threadId: string) => {
+    // Add to archived cache
+    setArchivedIds(prev => {
+      const next = new Set(prev);
+      next.add(threadId);
+      return next;
+    });
+    // Prevent re-processing and remove from list
+    setProcessedThreadIds(prev => new Set([...prev, threadId]));
+    setEmails(prev => prev.filter(e => e.threadId !== threadId));
+    // Reset index
+    setCurrentIndex(prev =>
+      prev >= Math.max(0, emails.length - 1) ? Math.max(0, prev - 1) : prev
+    );
+    // Invalidate caches
+    await queryClient.invalidateQueries({ queryKey: ['mail.listThreads'] });
+    await queryClient.invalidateQueries({ queryKey: ['mail.get', threadId] });
+    await queryClient.invalidateQueries({ queryKey: ['useThreads'] });
+    refetch();
+  }, [emails.length, queryClient, refetch]);

Then replace the repeated blocks in both handlers with:

-      // Add to archived set...
-      ...
-      // Force refetch...
-      refetch();
+      await finalizeThreadRemoval(currentEmail.threadId);

Also applies to: 308-355

🤖 Prompt for AI Agents
In apps/mail/app/(routes)/founder-mode/page.tsx around lines 242–306 (and also
308–355), the sendReplyAndArchive and archiveOnly handlers duplicate the same
post-action sequence; extract a single helper (e.g., finalizeArchiveAction) that
takes the threadId and any necessary context and performs: archivedIds.add +
sessionStorage update, setProcessedThreadIds update, remove the email from state
(setEmails filter), compute and setCurrentIndex correctly (handle emails.length
<= 1 and currentIndex bounds), invalidate the three query keys via
queryClient.invalidateQueries, and call refetch; then call that helper from both
handlers and remove the duplicated code blocks, ensuring the helper is
defined/closed over or passed the reactive values it needs and update the
useCallback dependencies accordingly.

Comment on lines +258 to +261
subject: currentEmail.subject.startsWith('Re:')
? currentEmail.subject
: `Re: ${currentEmail.subject}`,
message: formattedMessage,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Make “Re:” detection case-insensitive.

People love ‘RE:’ and ‘Re:’. Don’t duplicate prefixes.

Apply this diff:

-        subject: currentEmail.subject.startsWith('Re:') 
+        subject: /^re:/i.test(currentEmail.subject) 
           ? currentEmail.subject 
           : `Re: ${currentEmail.subject}`,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
subject: currentEmail.subject.startsWith('Re:')
? currentEmail.subject
: `Re: ${currentEmail.subject}`,
message: formattedMessage,
subject: /^re:/i.test(currentEmail.subject)
? currentEmail.subject
: `Re: ${currentEmail.subject}`,
message: formattedMessage,
🤖 Prompt for AI Agents
In apps/mail/app/(routes)/founder-mode/page.tsx around lines 258 to 261, the
subject prefix check uses startsWith('Re:') which is case-sensitive and will
duplicate 'RE:' or 're:'; change the check to a case-insensitive test (for
example using a regex like /^re:\s*/i or by lowercasing the trimmed subject) so
subjects that already begin with any case-variant of "Re:" are left unchanged,
and ensure you trim leading whitespace before testing to avoid false negatives;
if the test fails, prefix with "Re: " as before.

Comment on lines +265 to +274
// Use bulkArchive which marks as read AND removes from inbox
await bulkArchive.mutateAsync({
ids: [currentEmail.threadId],
});

// Also explicitly mark as read to be sure
await markAsRead.mutateAsync({
ids: [currentEmail.threadId],
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use the archivedIds setter; don’t mutate state + duplicate persistence.

Replace direct Set#add and manual sessionStorage writes with a single state update. Persistence now happens in the dedicated effect.

Apply this diff:

       // Use bulkArchive which marks as read AND removes from inbox
       await bulkArchive.mutateAsync({
         ids: [currentEmail.threadId],
       });
@@
       // Also explicitly mark as read to be sure
       await markAsRead.mutateAsync({
         ids: [currentEmail.threadId],
       });
@@
-      // Add to archived set to prevent re-fetching
-      archivedIds.add(currentEmail.threadId);
-      sessionStorage.setItem('founderMode_archivedIds', JSON.stringify(Array.from(archivedIds)));
+      // Add to archived set to prevent re-fetching
+      setArchivedIds(prev => {
+        const next = new Set(prev);
+        next.add(currentEmail.threadId);
+        return next;
+      });

Also applies to: 275-279

🤖 Prompt for AI Agents
In apps/mail/app/(routes)/founder-mode/page.tsx around lines 265-274 (and
similarly 275-279), replace the current pattern that mutates a Set and writes
directly to sessionStorage with a single state setter call: use
setArchivedIds(prev => { const next = new Set(prev);
next.add(currentEmail.threadId); return next; }) (or equivalent that creates a
new Set from prev and adds the id) and remove any direct sessionStorage.setItem
calls — persistence is handled by the existing effect; do not mutate prev
directly (no prev.add(...)) and do not duplicate persistence logic.

Comment on lines +541 to +552
const timeAgo = currentEmail ? formatDistanceToNow(new Date(currentEmail.receivedOn), { addSuffix: true }) : '';

return (
<div className="w-full min-h-screen bg-background overflow-y-auto">
<div className="max-w-3xl mx-auto px-8 py-6">
{/* Header */}
<div className="text-center mb-6">
<h1 className="text-3xl font-semibold mb-2">
{currentIndex + 1}/{emails.length} Unread Emails
</h1>
<p className="text-sm text-muted-foreground">{timeAgo}</p>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Guard against invalid dates before calling formatDistanceToNow.

receivedOn can be missing/invalid; avoid runtime errors.

Apply this diff:

-  const timeAgo = currentEmail ? formatDistanceToNow(new Date(currentEmail.receivedOn), { addSuffix: true }) : '';
+  const timeAgo =
+    currentEmail?.receivedOn
+      ? formatDistanceToNow(new Date(currentEmail.receivedOn), { addSuffix: true })
+      : '';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const timeAgo = currentEmail ? formatDistanceToNow(new Date(currentEmail.receivedOn), { addSuffix: true }) : '';
return (
<div className="w-full min-h-screen bg-background overflow-y-auto">
<div className="max-w-3xl mx-auto px-8 py-6">
{/* Header */}
<div className="text-center mb-6">
<h1 className="text-3xl font-semibold mb-2">
{currentIndex + 1}/{emails.length} Unread Emails
</h1>
<p className="text-sm text-muted-foreground">{timeAgo}</p>
</div>
const timeAgo =
currentEmail?.receivedOn
? formatDistanceToNow(new Date(currentEmail.receivedOn), { addSuffix: true })
: '';
return (
<div className="w-full min-h-screen bg-background overflow-y-auto">
<div className="max-w-3xl mx-auto px-8 py-6">
{/* Header */}
<div className="text-center mb-6">
<h1 className="text-3xl font-semibold mb-2">
{currentIndex + 1}/{emails.length} Unread Emails
</h1>
<p className="text-sm text-muted-foreground">{timeAgo}</p>
</div>
🤖 Prompt for AI Agents
In apps/mail/app/(routes)/founder-mode/page.tsx around lines 541 to 552, the
code calls formatDistanceToNow(new Date(currentEmail.receivedOn)) without
validating receivedOn; this can throw for missing/invalid dates. Guard by
checking currentEmail && currentEmail.receivedOn and that new
Date(currentEmail.receivedOn) is a valid date (e.g., create the Date and ensure
!isNaN(date.getTime())) before calling formatDistanceToNow, and fall back to an
empty string or a safe placeholder when the date is absent/invalid.

Comment on lines +586 to +589
>
<RefreshCw className={`h-4 w-4 ${currentEmail.isGenerating ? 'animate-spin' : ''}`} />
</Button>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Shortcut mismatch: UI says “Shift+Tab” but code triggers on plain Tab. Align them.

Two options:

  • Keep Tab behavior (recommended) and update texts, or
  • Require e.shiftKey in the handler.

Below updates texts to match current Tab behavior.

Apply this diff:

-                  title="Generate new reply (Shift+Tab)"
+                  title="Generate new reply (Tab)"
-            <div className="text-center text-xs text-muted-foreground">
-              Tab: Generate New AI Email • Cmd+Enter: Send & Archive • Cmd+Delete: Archive
-            </div>
+            <div className="text-center text-xs text-muted-foreground">
+              Tab: Generate New AI Email • Cmd+Enter: Send & Archive • Cmd+Delete: Archive
+            </div>

If you prefer Shift+Tab, modify the handler:

-      if (e.key === 'Tab' && !isEditable) {
+      if (e.key === 'Tab' && e.shiftKey && !isEditable) {

Also applies to: 609-612, 417-458

🤖 Prompt for AI Agents
In apps/mail/app/(routes)/founder-mode/page.tsx around lines 586-589 (and also
at 609-612 and 417-458), the UI text says "Shift+Tab" but the key handler
triggers on plain Tab; update the visible shortcut labels/tooltips/aria-labels
to "Tab" to match current behavior (or alternatively, if you prefer Shift+Tab,
change the key handler to require e.shiftKey), making sure every instance listed
(586-589, 609-612, 417-458) is updated consistently.

Comment on lines +596 to +602
<textarea
value={currentEmail.editedReply || ''}
onChange={(e) => updateEditedReply(e.target.value)}
className="w-full text-sm leading-normal resize-vertical bg-transparent border-0 focus:outline-none focus:ring-1 focus:ring-ring rounded p-2 min-h-[250px] h-[300px] max-h-[500px] overflow-y-auto"
placeholder="Type your reply here..."
/>
)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Tailwind conformance: use new utilities and valid classes.

  • outline-none was renamed; use outline-hidden (and focus variant).
  • resize-vertical isn’t a Tailwind utility; use resize-y.
  • rounded was renamed to rounded-sm per your guidelines.

This keeps you future-proof and consistent.

Apply this diff:

-                <textarea
+                <textarea
                   value={currentEmail.editedReply || ''}
                   onChange={(e) => updateEditedReply(e.target.value)}
-                  className="w-full text-sm leading-normal resize-vertical bg-transparent border-0 focus:outline-none focus:ring-1 focus:ring-ring rounded p-2 min-h-[250px] h-[300px] max-h-[500px] overflow-y-auto"
+                  className="w-full text-sm leading-normal resize-y bg-transparent border-0 outline-hidden focus:outline-hidden focus:ring-1 focus:ring-ring rounded-sm p-2 min-h-[250px] h-[300px] max-h-[500px] overflow-y-auto"
                   placeholder="Type your reply here..."
                 />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<textarea
value={currentEmail.editedReply || ''}
onChange={(e) => updateEditedReply(e.target.value)}
className="w-full text-sm leading-normal resize-vertical bg-transparent border-0 focus:outline-none focus:ring-1 focus:ring-ring rounded p-2 min-h-[250px] h-[300px] max-h-[500px] overflow-y-auto"
placeholder="Type your reply here..."
/>
)}
<textarea
value={currentEmail.editedReply || ''}
onChange={(e) => updateEditedReply(e.target.value)}
className="w-full text-sm leading-normal resize-y bg-transparent border-0 outline-hidden focus:outline-hidden focus:ring-1 focus:ring-ring rounded-sm p-2 min-h-[250px] h-[300px] max-h-[500px] overflow-y-auto"
placeholder="Type your reply here..."
/>
🤖 Prompt for AI Agents
In apps/mail/app/(routes)/founder-mode/page.tsx around lines 596 to 602, the
textarea uses deprecated/invalid Tailwind classes; replace outline-none with
focus:outline-hidden (and outline-hidden where applicable), change
resize-vertical to resize-y, and replace rounded with rounded-sm so the class
list becomes valid Tailwind utilities while preserving the same styling and
focus behavior.

Comment on lines 24 to 29
layout('(routes)/layout.tsx', [
route('/developer', '(routes)/developer/page.tsx'),
route('/founder-mode', '(routes)/founder-mode/page.tsx'),
layout(
'(routes)/mail/layout.tsx',
prefix('/mail', [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Place Founder Mode under the mail layout for coherence and shared chrome.

Founder Mode is conceptually part of Mail. Routing it under /mail keeps the layout, breadcrumbs, and sidebar active-states sane. Move the route into the /mail prefix block.

Apply this diff:

   layout('(routes)/layout.tsx', [
     route('/developer', '(routes)/developer/page.tsx'),
-    route('/founder-mode', '(routes)/founder-mode/page.tsx'),
     layout(
       '(routes)/mail/layout.tsx',
       prefix('/mail', [
         index('(routes)/mail/page.tsx'),
         route('/create', '(routes)/mail/create/page.tsx'),
         route('/compose', '(routes)/mail/compose/page.tsx'),
+        route('/founder-mode', '(routes)/founder-mode/page.tsx'),
         route('/under-construction/:path', '(routes)/mail/under-construction/[path]/page.tsx'),
         route('/:folder', '(routes)/mail/[folder]/page.tsx'),
       ]),
     ),

Then run to surface hard-coded links/tests using '/founder-mode':

Expected: all references updated to '/mail/founder-mode'.


🏁 Script executed:

#!/bin/bash
rg -nP -C2 "/founder-mode\b" --type=ts --type=tsx --type=mdx

Length of output: 90


Alright, let’s refine that search and catch every stray link. Try this instead:


🏁 Script executed:

#!/bin/bash
# Search for hard-coded '/founder-mode' in .ts, .tsx, and .mdx files with context
rg -nP -C2 "/founder-mode\b" -g '*.ts' -g '*.tsx' -g '*.mdx'

Length of output: 723


Update Founder Mode links to use the /mail prefix

We still have two hard-coded references to /founder-mode that need updating:

• apps/mail/app/routes.ts
– Remove the standalone route at line 26 and insert it under the /mail prefix as shown in the original diff.

• apps/mail/config/navigation.ts (around line 62)

-           url: '/founder-mode',
+           url: '/mail/founder-mode',

Make sure both are changed so that routing, navigation, breadcrumbs, and chrome all stay in sync.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
layout('(routes)/layout.tsx', [
route('/developer', '(routes)/developer/page.tsx'),
route('/founder-mode', '(routes)/founder-mode/page.tsx'),
layout(
'(routes)/mail/layout.tsx',
prefix('/mail', [
url: '/mail/founder-mode',
🤖 Prompt for AI Agents
In apps/mail/app/routes.ts around lines 24 to 29, the standalone route for
'/founder-mode' must be removed and moved under the mail prefix: delete the
route('/founder-mode', ...) at line 26 and include it inside the prefix('/mail',
[...]) block so '/mail/founder-mode' is registered; also update
apps/mail/config/navigation.ts around line 62 to change any hard-coded
'/founder-mode' links to '/mail/founder-mode' so routing, navigation,
breadcrumbs, and chrome remain consistent.

Comment on lines +59 to +65
{
id: 'founder-mode',
title: 'Founder Mode',
url: '/founder-mode',
icon: Zap,
shortcut: 'g + f',
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Title must use i18n message key, not a raw string.

The file explicitly states titles must be message keys. Use a translated message (e.g., m'navigation.sidebar.founderMode') instead of 'Founder Mode'.

Apply this diff:

           {
             id: 'founder-mode',
-            title: 'Founder Mode',
+            title: m['navigation.sidebar.founderMode'](),
             url: '/founder-mode',
             icon: Zap,
             shortcut: 'g + f',
           },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
id: 'founder-mode',
title: 'Founder Mode',
url: '/founder-mode',
icon: Zap,
shortcut: 'g + f',
},
{
id: 'founder-mode',
title: m['navigation.sidebar.founderMode'](),
url: '/founder-mode',
icon: Zap,
shortcut: 'g + f',
},
🤖 Prompt for AI Agents
In apps/mail/config/navigation.ts around lines 59 to 65, the navigation item
uses a raw string for title ('Founder Mode') but titles must be i18n message
keys; replace the raw string with the translated message call (e.g.,
m['navigation.sidebar.founderMode']()) and ensure the messages object `m` is in
scope or imported where this file defines navigation so the title returns the
localized string.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

High Priority High Priority Work

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant