-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
founder mode #2001
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: staging
Are you sure you want to change the base?
founder mode #2001
Conversation
WalkthroughAdds 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested labels
Suggested reviewers
Poem
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 unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this 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', |
There was a problem hiding this comment.
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's i18n requirement.
(Based on your team's feedback about keeping navigation titles sourced from message keys for localization consistency.)</comment>
<file context>
@@ -56,6 +56,13 @@ export const navigationConfig: Record<string, NavConfig> = {
icon: Inbox,
shortcut: 'g + i',
},
+ {
+ id: 'founder-mode',
+ title: 'Founder Mode',
+ url: '/founder-mode',
+ icon: Zap,
</file context>
title: 'Founder Mode', | |
title: m['navigation.sidebar.founderMode'](), |
variant="ghost" | ||
onClick={regenerateReply} | ||
disabled={currentEmail.isGenerating} | ||
title="Generate new reply (Shift+Tab)" |
There was a problem hiding this comment.
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 @@
+'use client';
+
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useActiveConnection } from '@/hooks/use-connections';
+import { useTRPC } from '@/providers/query-provider';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { Button } from '@/components/ui/button';
+import { Loader2, X, Check, RefreshCw } from 'lucide-react';
+import { useNavigate } from 'react-router';
</file context>
|
||
// Use sessionStorage to persist archived IDs across refreshes | ||
const [archivedIds] = useState<Set<string>>(() => { | ||
const stored = sessionStorage.getItem('founderMode_archivedIds'); |
There was a problem hiding this comment.
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 @@
+'use client';
+
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useActiveConnection } from '@/hooks/use-connections';
+import { useTRPC } from '@/providers/query-provider';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { Button } from '@/components/ui/button';
+import { Loader2, X, Check, RefreshCw } from 'lucide-react';
+import { useNavigate } from 'react-router';
</file context>
There was a problem hiding this 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 sectionWe 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 ofitem.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 matchesconfig.path
• Confirm in app-sidebar.tsx (lines ~48-50) and nav-main.tsx (lines ~227-230) that active logic now correctly flags Founder Mode underProposed 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.
📒 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 asstarting
,not-*
,inert
,nth-*
,in-*
,open
(for:popover-open
), and**
for all descendants.
Do not use deprecated utilities likebg-opacity-*
,text-opacity-*
,border-opacity-*
, anddivide-opacity-*
; use the new syntax (e.g.,bg-black/50
).
Use renamed utilities:shadow-sm
is nowshadow-xs
,shadow
is nowshadow-sm
,drop-shadow-sm
is nowdrop-shadow-xs
,drop-shadow
is nowdrop-shadow-sm
,blur-sm
is nowblur-xs
,blur
is nowblur-sm
,rounded-sm
is nowrounded-xs
,rounded
is nowrounded-sm
,outline-none
is nowoutline-hidden
.
Usebg-(--brand-color)
syntax for CSS variables in arbitrary values instead ofbg-[--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 requiredThe TRPC router definitions in
apps/server/src/trpc/routes/mail.ts
use:.input( z.object({ ids: z.string().array(),for both
bulkArchive
andmarkAsRead
, so your client calls passing{ ids: [...] }
are already correct.
interface EmailData { | ||
threadId: string; | ||
subject: string; | ||
sender: string; | ||
senderEmail: string; | ||
content: string; | ||
receivedOn: string; | ||
generatedReply: string; | ||
editedReply: string; | ||
isGenerating: boolean; | ||
} |
There was a problem hiding this comment.
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.
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.
// 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(); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
// 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.
// 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]); |
There was a problem hiding this comment.
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.
subject: currentEmail.subject.startsWith('Re:') | ||
? currentEmail.subject | ||
: `Re: ${currentEmail.subject}`, | ||
message: formattedMessage, |
There was a problem hiding this comment.
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.
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.
// 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], | ||
}); | ||
|
There was a problem hiding this comment.
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.
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> |
There was a problem hiding this comment.
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.
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.
> | ||
<RefreshCw className={`h-4 w-4 ${currentEmail.isGenerating ? 'animate-spin' : ''}`} /> | ||
</Button> | ||
</div> |
There was a problem hiding this comment.
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.
<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..." | ||
/> | ||
)} |
There was a problem hiding this comment.
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.
<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.
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', [ |
There was a problem hiding this comment.
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.
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.
{ | ||
id: 'founder-mode', | ||
title: 'Founder Mode', | ||
url: '/founder-mode', | ||
icon: Zap, | ||
shortcut: 'g + f', | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
{ | |
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.
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.
Description
Please provide a clear description of your changes.
Type of Change
Please delete options that are not relevant.
Areas Affected
Please check all that apply:
Testing Done
Describe the tests you've done:
Security Considerations
For changes involving data or authentication:
Checklist
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.
Summary by CodeRabbit