Skip to content

Automation rules improvements#1065

Closed
jshwrnr wants to merge 139 commits intoelie222:stagingfrom
jshwrnr:feat/rules-improvements
Closed

Automation rules improvements#1065
jshwrnr wants to merge 139 commits intoelie222:stagingfrom
jshwrnr:feat/rules-improvements

Conversation

@jshwrnr
Copy link
Collaborator

@jshwrnr jshwrnr commented Dec 4, 2025

Add meeting briefs feature and upgrade automation rules UI, stats APIs, and LLM config including Ollama with new cron endpoint at 15 minutes and watch renewal every 6 hours

Introduce Meeting Briefs with settings, cron endpoints, email template, providers for Google/Microsoft calendars, and server actions; refactor Automation Rules UI (steps, actions, menus) and table; add stats APIs (by-period, response time) with controllers and OpenAPI; integrate new Chat open flow; add Outlook unread-preserving draft and provider thread queries; and enable Ollama model selection via env with tests and schema updates.

📍Where to Start

Start with Meeting Briefs flow in the API entrypoint GET /api/meeting-briefs and controller pipeline via apps/web/app/api/meeting-briefs/route.ts, then review the processing workflow in apps/web/utils/meeting-briefs/process.ts.


Macroscope summarized c1c2e2c.

Summary by CodeRabbit

  • New Features

    • Meeting Briefs: Automatic email briefings for calendar events with guest research and external participant context.
    • Redesigned rule configuration UI with improved organization and modular components.
    • Meeting Briefs navigation added to sidebar.
  • Bug Fixes

    • Fixed Outlook reply drafts incorrectly marking original messages as read.
  • Improvements

    • Enhanced AI capabilities with Perplexity API integration.
    • Upgraded Ollama local LLM support to v2.
    • Improved error handling and logging across stats and calendar features.

✏️ Tip: You can customize this high-level summary in your review settings.


Note

Introduces a step-based rule builder with improved action UX, adds setup dashboard metrics (and API), streamlines the rules list, and adds a Home link.

  • Rule Builder UI:
    • Introduces RuleSectionCard, RuleStep, RuleSteps plus ConditionSteps and ActionSteps for step-based editing in RuleForm.
    • Enhances actions UI: icons per action, expandable fields, delay controls (DelayInputControls), AI/manual label selection, and webhook label text.
    • Refactors validation and condition handling: allow multiple STATIC conditions (one field each) and prevent duplicates; condition.ts splits/merges static conditions accordingly.
  • Rules List:
    • Removes conditions column; right-aligns action badges; keeps actions sorted and compact.
  • Setup/Home:
    • Adds StatsCardGrid and displays inbox metrics (emails processed, rules executed, inbox coverage) on setup page.
    • Extends GET /api/user/setup-progress to return the above metrics.
    • Adds "Home" link to sidebar pointing to setup.
  • Components:
    • Combobox/LabelCombobox: support custom footer and improved UX.
    • AddRuleDialog: right-aligned trigger button.

Written by Cursor Bugbot for commit 096ea69. Configure here.

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: 8

Caution

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

⚠️ Outside diff range comments (1)
.cursor/rules/notes.mdc (1)

1-7: Add main title heading and use kebab-case filename; populate frontmatter description.

The file is missing a main title heading and doesn't follow cursor rule conventions. The filename should be descriptive and in kebab-case, and the YAML frontmatter description field should be populated.

Apply the following changes:

  1. Rename the file from notes.mdc to developer-notes.mdc (or similar descriptive kebab-case name).

  2. Update the YAML frontmatter and add a main title heading:

 ---
-description: 
+description: Development guidelines for project contributors
 globs: 
 alwaysApply: true
 ---
+# Developer Notes
+
  1. Restructure the content with proper markdown formatting (headings for sections):
+## Guidelines
+
 - Do not try and run the project via `dev` or `build` command unless I explicitly ask you to.
 - Prefer self-documenting code over comments; use descriptive variable and function names instead of explaining intent with comments.

As per coding guidelines, cursor rule files must have a main title heading and structured content with step-by-step instructions, code examples, and guidelines.

♻️ Duplicate comments (1)
apps/web/utils/outlook/message.ts (1)

115-132: Fix empty-query edge case in sanitizeOutlookSearchQuery (still present) and confirm KQL helper behavior

sanitizeKqlValue and the KQL short-circuit look good, but the earlier issue with queries that sanitize to empty still exists in the non-KQL path:

  • Example: input "???"normalized = "???"
  • After disallowed-char and whitespace normalization: sanitized = ""
  • Escaping keeps it empty, then it is wrapped as "\"\"", which is truthy
  • effectiveSearchQuery = cleanedSearchQuery || undefined will use "" as a literal phrase query instead of treating it as “no query”

This is the same scenario flagged in the previous review comment and is still reachable when the query is not KQL-shaped. To align behavior with expectations (“no usable search term” → no $search), you should short‑circuit when the cleaned value becomes empty, before escaping/quoting.

Suggested fix:

 function sanitizeOutlookSearchQuery(query: string): {
   sanitized: string;
   wasSanitized: boolean;
 } {
   const normalized = query.trim();
   if (!normalized) {
     return { sanitized: "", wasSanitized: false };
   }

@@
   // Remove disallowed characters
   let sanitized = normalized
     .replace(OUTLOOK_SEARCH_DISALLOWED_CHARS, " ")
     .replace(/\s+/g, " ")
     .trim();

+  // If nothing remains after removing disallowed characters and collapsing
+  // whitespace, treat this as “no query” instead of issuing an empty phrase search.
+  if (!sanitized) {
+    return { sanitized: "", wasSanitized: false };
+  }
+
   // Escape backslashes and double quotes for KQL
   sanitized = sanitized.replace(/\\/g, "\\\\").replace(/"/g, '\\"');

   // Wrap in double quotes to treat as literal phrase search
   // This prevents KQL from interpreting special characters like - . and numbers
   sanitized = `"${sanitized}"`;

   return {
     sanitized,
-    wasSanitized: true, // Always true now since we're wrapping in quotes
+    wasSanitized: true,
   };
 }

This keeps your new KQL handling intact (KQL queries bypass this path entirely) while preventing accidental empty phrase searches for fully stripped user input.

Also applies to: 143-168

🧹 Nitpick comments (14)
apps/web/utils/actions/ai-rule.ts (1)

44-45: Switch to SafeError here is correct; consider adding logging for consistency

Using SafeError instead of a generic Error for these user-facing guard failures is aligned with the project’s security guidance and the rest of this file’s patterns. The messages are generic enough and avoid leaking internal details.

For consistency with saveRulesPromptAction / createRulesAction, you might also log these cases before throwing (e.g., logger.error("Email account not found") / logger.error("Provider not found")) so missing-context issues are easier to trace in logs.

Based on learnings, this matches the recommendation to prefer SafeError for user-visible failures.

apps/web/utils/cron.ts (1)

6-34: Stronger handling for missing CRON_SECRET is good; consider mirroring it in getCronSecretHeader.

The early return + log in hasCronSecret and hasPostCronSecret is a solid hardening step and avoids treating a missing secret as just another unauthorized request. One follow-up to consider is updating getCronSecretHeader() to either log and throw or log and return null when env.CRON_SECRET is unset, so you don’t accidentally send Authorization: Bearer undefined from misconfigured internal callers.

apps/web/utils/ai/meeting-briefs/generate-briefing.ts (1)

16-61: Consider adding error handling for LLM generation failures.

Per coding guidelines, LLM feature functions should implement error handling with try/catch and consider retry logic for transient failures. Currently, errors propagate directly to the caller.

+import { withRetry } from "@/utils/retry";
+import { createScopedLogger } from "@/utils/logger";
+
+const logger = createScopedLogger("ai/meeting-briefings");
+
 export async function aiGenerateMeetingBriefing({
   briefingData,
   emailAccount,
 }: {
   briefingData: MeetingBriefingData;
   emailAccount: EmailAccountWithAI;
 }): Promise<string> {
+  try {
     // ... existing implementation ...
-  return result.object.briefing;
+    return result.object.briefing;
+  } catch (error) {
+    logger.error("Failed to generate meeting briefing", { error });
+    throw error;
+  }
 }
apps/web/utils/redis/perplexity-research.ts (1)

83-84: Consider hashing email in log messages to avoid PII exposure.

Per security guidelines, logs should not contain PII like email addresses. Consider using a hash or omitting the email.

   } catch (error) {
-    logger.error("Failed to get cached perplexity research", { email, error });
+    logger.error("Failed to get cached perplexity research", { error });
     return null;
   }
apps/web/utils/meeting-briefs/process.ts (1)

89-117: Consider adding ownership validation to the emailAccount query.

Per security guidelines, database queries should include ownership filters. While emailAccountId likely comes from authenticated middleware, adding a userId filter provides defense in depth.

If the userId is available in context, consider:

   const emailAccount = await prisma.emailAccount.findUnique({
-    where: { id: emailAccountId },
+    where: { 
+      id: emailAccountId,
+      // Add userId if available for defense in depth
+    },
     select: {
       // ...
     },
   });
apps/web/utils/meeting-briefs/gather-context.ts (1)

103-109: Consider masking or hashing email in logs.

Logging the raw attendee.email could expose PII in logs. Consider using a hashed or truncated form for traceability without full exposure, consistent with privacy best practices.

     }).catch((error) => {
       logger.warn("Failed to research guest", {
-        email: attendee.email,
+        emailHash: attendee.email.slice(0, 3) + "***",
         error,
       });
       return null;
     }),
apps/web/utils/calendar/providers/google-events.ts (1)

91-122: LGTM with a minor observation.

The event parsing logic correctly extracts all relevant fields and handles video conference links from multiple sources. The fallback to Date.now() for missing dates ensures the function always returns valid Date objects.

Consider logging a warning when event.id is missing (Line 108), as an empty string ID might cause issues in downstream code that expects unique identifiers:

+    if (!event.id) {
+      logger.warn("Event missing ID", { summary: event.summary });
+    }
+
     return {
       id: event.id || "",

Alternatively, you could generate a temporary ID or throw an error if the ID is critical for your use case.

apps/web/app/(app)/[emailAccountId]/briefs/page.tsx (1)

282-290: Status badge may not handle all enum values gracefully.

The current implementation only distinguishes between "SENT" (green) and everything else (red). According to the migration summary, the enum will be extended to include PENDING and SKIPPED statuses. Consider handling these cases explicitly for better UX.

 <span
-  className={`text-xs px-2 py-1 rounded ${
-    briefing.status === "SENT"
-      ? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
-      : "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
-  }`}
+  className={cn(
+    "text-xs px-2 py-1 rounded",
+    briefing.status === "SENT" && "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
+    briefing.status === "FAILED" && "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
+    briefing.status === "PENDING" && "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
+    briefing.status === "SKIPPED" && "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200",
+  )}
 >
apps/web/utils/meeting-briefs/send-briefing.ts (1)

46-62: Consider validating RESEND_FROM_EMAIL when using Resend.

The code checks for RESEND_API_KEY but doesn't verify that RESEND_FROM_EMAIL is also configured. If RESEND_FROM_EMAIL is undefined or empty, the email send will likely fail with an unclear error.

   // Try Resend first if configured
-  if (env.RESEND_API_KEY) {
+  if (env.RESEND_API_KEY && env.RESEND_FROM_EMAIL) {
     log.info("Sending briefing via Resend");

Alternatively, add explicit validation with a clearer error message before attempting to send.

apps/web/app/api/user/calendar/upcoming-events/route.ts (1)

22-26: Consider handling individual provider failures gracefully.

If one calendar provider fails (e.g., expired token), Promise.all will reject the entire request. Consider using Promise.allSettled to return events from successful providers while logging failures.

-  const providerEvents = await Promise.all(
-    providers.map(async (provider) => {
-      return provider.fetchEvents({ maxResults: 3 });
-    }),
-  );
+  const results = await Promise.allSettled(
+    providers.map((provider) => provider.fetchEvents({ maxResults: 3 })),
+  );
+
+  const providerEvents = results
+    .filter((r): r is PromiseFulfilledResult<Awaited<ReturnType<typeof providers[0]['fetchEvents']>>> => r.status === "fulfilled")
+    .map((r) => r.value);
apps/web/prisma/schema.prisma (1)

937-952: Consider adding updatedAt field for consistency.

Most models in the schema include updatedAt DateTime @updatedAt for tracking modifications. This is useful for debugging and auditing, especially if the status field changes over time.

 model MeetingBriefing {
   id        String   @id @default(cuid())
   createdAt DateTime @default(now())
+  updatedAt DateTime @updatedAt

   calendarEventId String // External event ID from Google/Microsoft
apps/web/utils/calendar/providers/microsoft-events.ts (2)

55-63: Extract duplicate type definition.

The MicrosoftEvent type is defined inline twice with slightly different fields. Extract this to a single shared type at the top of the file to eliminate duplication and ensure consistency.

Apply this diff:

+type MicrosoftEvent = {
+  id?: string;
+  subject?: string;
+  bodyPreview?: string;
+  start?: { dateTime?: string };
+  end?: { dateTime?: string };
+  attendees?: Array<{
+    emailAddress?: { address?: string; name?: string };
+  }>;
+  location?: { displayName?: string };
+  webLink?: string;
+  onlineMeeting?: { joinUrl?: string };
+  onlineMeetingUrl?: string;
+};
+
 export class MicrosoftCalendarEventProvider implements CalendarEventProvider {
   private readonly connection: MicrosoftCalendarConnectionParams;

   constructor(connection: MicrosoftCalendarConnectionParams) {
     this.connection = connection;
   }

   private async getClient(): Promise<Client> {
     return getCalendarClientWithRefresh({
       accessToken: this.connection.accessToken,
       refreshToken: this.connection.refreshToken,
       expiresAt: this.connection.expiresAt,
       emailAccountId: this.connection.emailAccountId,
     });
   }

   async fetchEventsWithAttendee({
     attendeeEmail,
     timeMin,
     timeMax,
     maxResults,
   }: {
     attendeeEmail: string;
     timeMin: Date;
     timeMax: Date;
     maxResults: number;
   }): Promise<CalendarEvent[]> {
     const client = await this.getClient();

     const response = await client
       .api("/me/calendar/events")
       .filter(
         `start/dateTime ge '${timeMin.toISOString()}' and end/dateTime le '${timeMax.toISOString()}'`,
       )
       .top(maxResults * 3) // Fetch more to filter by attendee
       .orderby("start/dateTime")
       .get();

     const events = response.value || [];

-    type MicrosoftEvent = {
-      id?: string;
-      subject?: string;
-      start?: { dateTime?: string };
-      end?: { dateTime?: string };
-      attendees?: Array<{
-        emailAddress?: { address?: string; name?: string };
-      }>;
-    };
-
     // Filter to events that have this attendee
     return events
       .filter((event: MicrosoftEvent) =>
         event.attendees?.some(
           (a) =>
             a.emailAddress?.address?.toLowerCase() ===
             attendeeEmail.toLowerCase(),
         ),
       )
       .slice(0, maxResults)
       .map((event: MicrosoftEvent) => this.parseEvent(event));
   }

   async fetchEvents({
     timeMin = new Date(),
     timeMax,
     maxResults,
   }: {
     timeMin?: Date;
     timeMax?: Date;
     maxResults?: number;
   }): Promise<CalendarEvent[]> {
     const client = await this.getClient();

     const filterParts = [
       timeMin ? `start/dateTime ge '${timeMin.toISOString()}'` : "",
       timeMax ? `end/dateTime le '${timeMax.toISOString()}'` : "",
     ].filter(Boolean);

     const response = await client
       .api("/me/calendar/events")
       .filter(filterParts.join(" and "))
       .top(maxResults || 100)
       .orderby("start/dateTime")
       .get();

     const events = response.value || [];

-    type MicrosoftEvent = {
-      id?: string;
-      subject?: string;
-      bodyPreview?: string;
-      start?: { dateTime?: string };
-      end?: { dateTime?: string };
-      attendees?: Array<{
-        emailAddress?: { address?: string; name?: string };
-      }>;
-      location?: { displayName?: string };
-      webLink?: string;
-      onlineMeeting?: { joinUrl?: string };
-      onlineMeetingUrl?: string;
-    };
-
     return events.map((event: MicrosoftEvent) => this.parseEvent(event));
   }

Also applies to: 103-116


121-134: Update method signature to use extracted type.

After extracting the MicrosoftEvent type (per previous comment), update the parseEvent method signature to use it instead of the inline type definition.

Apply this diff:

-  private parseEvent(event: {
-    id?: string;
-    subject?: string;
-    bodyPreview?: string;
-    start?: { dateTime?: string };
-    end?: { dateTime?: string };
-    attendees?: Array<{
-      emailAddress?: { address?: string; name?: string };
-    }>;
-    location?: { displayName?: string };
-    webLink?: string;
-    onlineMeeting?: { joinUrl?: string };
-    onlineMeetingUrl?: string;
-  }) {
+  private parseEvent(event: MicrosoftEvent) {
     return {
apps/web/utils/email/microsoft.ts (1)

11-12: Normalize participant email once and reuse for both KQL search and header comparison

The overall getThreadsWithParticipant flow is solid (participants KQL + header-level verification + per‑thread error handling), but you currently:

  • Sanitize participantEmail for KQL via sanitizeKqlValue, and
  • Compare headers against the raw participantEmail (lowercased) instead of a normalized address.

If callers ever pass display‑style values like "Name <user@example.com>", the KQL search may still work, but header comparisons against participantLower will fail even though extractEmailAddress on headers returns just "user@example.com".

You can make this more robust by normalizing once and reusing that value:

-import {
-  getMessage,
-  getMessages,
-  queryBatchMessages,
-  getFolderIds,
-  convertMessage,
-  MESSAGE_SELECT_FIELDS,
-  sanitizeKqlValue,
-} from "@/utils/outlook/message";
+import {
+  getMessage,
+  getMessages,
+  queryBatchMessages,
+  getFolderIds,
+  convertMessage,
+  MESSAGE_SELECT_FIELDS,
+  sanitizeKqlValue,
+} from "@/utils/outlook/message";
@@
   async getThreadsWithParticipant(options: {
     participantEmail: string;
     maxThreads?: number;
   }): Promise<EmailThread[]> {
-    const { participantEmail, maxThreads = 5 } = options;
+    const { participantEmail, maxThreads = 5 } = options;
+
+    const participantAddress =
+      extractEmailAddress(participantEmail) || participantEmail.trim();
+    if (!participantAddress) {
+      return [];
+    }
 
@@
-    //
-    const sanitizedEmail = sanitizeKqlValue(participantEmail);
-    const searchQuery = `participants:${sanitizedEmail}`;
+    //
+    const sanitizedEmail = sanitizeKqlValue(participantAddress);
+    if (!sanitizedEmail) {
+      return [];
+    }
+    const searchQuery = `participants:${sanitizedEmail}`;
@@
-    const participantLower = participantEmail.toLowerCase().trim();
+    const participantLower = participantAddress.toLowerCase();
@@
-      const fromEmail = extractEmailAddress(h.from || "").toLowerCase();
+      const fromEmail = extractEmailAddress(h.from || "").toLowerCase();
       if (fromEmail === participantLower) return true;
@@
-      const toAddresses = (h.to || "")
+      const toAddresses = (h.to || "")
         .split(",")
         .map((addr) => extractEmailAddress(addr.trim()).toLowerCase())
@@
-      const ccAddresses = (h.cc || "")
+      const ccAddresses = (h.cc || "")
         .split(",")
         .map((addr) => extractEmailAddress(addr.trim()).toLowerCase())

This keeps your KQL query safe, ensures header comparisons succeed for common input formats, and cheaply handles pathological inputs (early return if no usable address).

Also applies to: 870-943

Comment on lines +48 to +51
const log = logger.with({
emailAccountId: emailAccount.id,
email: emailAccount.email,
});
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 | 🟡 Minor

Consider hashing the email in logs to comply with PII guidelines.

Per coding guidelines, logging user identifiers like emails can be a compliance/privacy concern. The logger already includes emailAccountId which should be sufficient for debugging.

     const log = logger.with({
       emailAccountId: emailAccount.id,
-      email: emailAccount.email,
     });
📝 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 log = logger.with({
emailAccountId: emailAccount.id,
email: emailAccount.email,
});
const log = logger.with({
emailAccountId: emailAccount.id,
});
🤖 Prompt for AI Agents
In apps/web/app/api/meeting-briefs/route.ts around lines 48-51, the logger
currently includes the raw email which is PII; replace the plain email with a
non-reversible hashed identifier (or remove it) to comply with PII guidelines.
Compute a deterministic hash (e.g., HMAC-SHA256 using the app secret or at
minimum SHA-256) of emailAccount.email and pass that hashed value as hashedEmail
to logger.with while keeping emailAccountId unchanged; ensure the hashing
happens before logging and that no plaintext email is written to any logs or
error messages.

Comment on lines +8 to +17
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
role="list"
data-slot="item-group"
className={cn("group/item-group flex flex-col", className)}
{...props}
/>
);
}
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 | 🟠 Major

Add role="listitem" to Item component for ARIA compliance.

ItemGroup uses role="list" (line 11), but the Item component does not include role="listitem". According to ARIA specifications, elements with role="list" must contain elements with role="listitem" to properly convey list semantics to assistive technologies.

Apply this diff to add the required role to Item:

 function Item({
   className,
   variant = "default",
   size = "default",
   asChild = false,
   ...props
 }: React.ComponentProps<"div"> &
   VariantProps<typeof itemVariants> & { asChild?: boolean }) {
   const Comp = asChild ? Slot : "div";
   return (
     <Comp
       data-slot="item"
       data-variant={variant}
       data-size={size}
+      role="listitem"
       className={cn(itemVariants({ variant, size, className }))}
       {...props}
     />
   );
 }

As per coding guidelines: "Include all required ARIA attributes for elements with ARIA roles."

Also applies to: 54-72

🤖 Prompt for AI Agents
In apps/web/components/ui/item.tsx around lines 8-17 (and also apply the same
change to the Item implementation at lines 54-72), the ItemGroup uses
role="list" but the Item component lacks role="listitem"; update the Item
component to include role="listitem" on the root element (ensuring existing
props and className are preserved) so each child of the ItemGroup conveys
correct ARIA list-item semantics to assistive technologies.

Comment on lines +33 to +52
const itemVariants = cva(
"group/item [a]:hover:bg-accent/50 focus-visible:border-ring focus-visible:ring-ring/50 [a]:transition-colors flex flex-wrap items-center rounded-md border border-transparent text-sm outline-none transition-colors duration-100 focus-visible:ring-[3px]",
{
variants: {
variant: {
default: "bg-transparent",
outline: "border-border",
muted: "bg-muted/50",
},
size: {
default: "gap-4 p-4 ",
sm: "gap-2.5 px-4 py-3",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find Tailwind configuration files
fd -type f -name "tailwind.config.*" -o -name "tailwind.*.js" -o -name "tailwind.*.ts"

Repository: elie222/inbox-zero

Length of output: 232


🏁 Script executed:

# Search for custom Tailwind variant definitions or [a]: pattern usage
rg -n "\[a\]:" --type ts --type tsx --type js

Repository: elie222/inbox-zero

Length of output: 89


🏁 Script executed:

# Check project root for Tailwind config
ls -la | grep -i tailwind

Repository: elie222/inbox-zero

Length of output: 44


🏁 Script executed:

# Find Tailwind config files
find . -maxdepth 3 -name "tailwind.config.*" -o -name "*.config.ts" -o -name "*.config.js" | grep -i tailwind

Repository: elie222/inbox-zero

Length of output: 91


🏁 Script executed:

# Search for [a]: pattern in the codebase
rg "\[a\]:" -g "*.ts" -g "*.tsx" -g "*.js" -g "*.jsx"

Repository: elie222/inbox-zero

Length of output: 349


🏁 Script executed:

# Check for Tailwind configuration in root
ls -la | head -20

Repository: elie222/inbox-zero

Length of output: 1225


🏁 Script executed:

# Search for CVA usage and patterns similar to [a]:
rg "group/item|has-\[" --max-count=5 -A 2

Repository: elie222/inbox-zero

Length of output: 2414


🏁 Script executed:

# Read the Tailwind config file
cat ./apps/web/tailwind.config.js

Repository: elie222/inbox-zero

Length of output: 6918


🏁 Script executed:

# Search for custom variants or plugins in the config
rg "addVariant|matchVariant|plugin" ./apps/web/tailwind.config.js -A 5

Repository: elie222/inbox-zero

Length of output: 230


🏁 Script executed:

# Check if there's any documentation or comments about the [a]: syntax
rg "\[a\]" . -B 3 -A 3

Repository: elie222/inbox-zero

Length of output: 1483


Replace [a]: with [&_a]: for correct Tailwind descendant selector syntax.

The classes [a]:hover:bg-accent/50 and [a]:transition-colors on line 34 use invalid Tailwind syntax. The [a]: prefix is neither a standard Tailwind variant nor defined as a custom variant in the project's configuration. To target anchor element children, use the correct arbitrary selector syntax [&_a]: (with the & reference and underscore descendant combinator):

 const itemVariants = cva(
-  "group/item [a]:hover:bg-accent/50 focus-visible:border-ring focus-visible:ring-ring/50 [a]:transition-colors flex flex-wrap items-center rounded-md border border-transparent text-sm outline-none transition-colors duration-100 focus-visible:ring-[3px]",
+  "group/item [&_a]:hover:bg-accent/50 focus-visible:border-ring focus-visible:ring-ring/50 [&_a]:transition-colors flex flex-wrap items-center rounded-md border border-transparent text-sm outline-none transition-colors duration-100 focus-visible:ring-[3px]",
📝 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 itemVariants = cva(
"group/item [a]:hover:bg-accent/50 focus-visible:border-ring focus-visible:ring-ring/50 [a]:transition-colors flex flex-wrap items-center rounded-md border border-transparent text-sm outline-none transition-colors duration-100 focus-visible:ring-[3px]",
{
variants: {
variant: {
default: "bg-transparent",
outline: "border-border",
muted: "bg-muted/50",
},
size: {
default: "gap-4 p-4 ",
sm: "gap-2.5 px-4 py-3",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
const itemVariants = cva(
"group/item [&_a]:hover:bg-accent/50 focus-visible:border-ring focus-visible:ring-ring/50 [&_a]:transition-colors flex flex-wrap items-center rounded-md border border-transparent text-sm outline-none transition-colors duration-100 focus-visible:ring-[3px]",
{
variants: {
variant: {
default: "bg-transparent",
outline: "border-border",
muted: "bg-muted/50",
},
size: {
default: "gap-4 p-4 ",
sm: "gap-2.5 px-4 py-3",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
🤖 Prompt for AI Agents
In apps/web/components/ui/item.tsx around lines 33 to 52, the Tailwind arbitrary
selector `[a]:` is invalid; replace the two occurrences
(`[a]:hover:bg-accent/50` and `[a]:transition-colors`) with the correct
descendant selector `[&_a]:hover:bg-accent/50` and `[&_a]:transition-colors` so
the styles target anchor children properly using the `&` reference and
descendant combinator.

Comment on lines +173 to +184
function messageIncludesEmail(
message: ParsedMessage,
emailLower: string,
): boolean {
const headers = message.headers;
return (
headers.from.toLowerCase().includes(emailLower) ||
headers.to.toLowerCase().includes(emailLower) ||
(headers.cc?.toLowerCase().includes(emailLower) ?? false) ||
(headers.bcc?.toLowerCase().includes(emailLower) ?? false)
);
}
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 | 🟡 Minor

Potential false positives with includes() for email matching.

Using includes() on header strings could match partial email addresses. For example, searching for "john@example.com" would also match "john@example.com.attacker.com". Consider extracting email addresses from headers and comparing exactly.

+import { extractEmailAddresses } from "@/utils/email";
+
 function messageIncludesEmail(
   message: ParsedMessage,
   emailLower: string,
 ): boolean {
   const headers = message.headers;
-  return (
-    headers.from.toLowerCase().includes(emailLower) ||
-    headers.to.toLowerCase().includes(emailLower) ||
-    (headers.cc?.toLowerCase().includes(emailLower) ?? false) ||
-    (headers.bcc?.toLowerCase().includes(emailLower) ?? false)
-  );
+  const allEmails = [
+    ...extractEmailAddresses(headers.from),
+    ...extractEmailAddresses(headers.to),
+    ...extractEmailAddresses(headers.cc ?? ""),
+    ...extractEmailAddresses(headers.bcc ?? ""),
+  ].map(e => e.toLowerCase());
+  return allEmails.includes(emailLower);
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/web/utils/ai/meeting-briefs/generate-briefing.ts around lines 173-184,
the current messageIncludesEmail uses string.includes which can produce false
positives (e.g., "john@example.com.attacker.com"); instead parse the
from/to/cc/bcc header values into discrete email addresses, normalize each to
lowercase, and check for an exact equality match against emailLower; ensure you
handle comma/semicolon separated lists and optional headers, and return true if
any parsed address equals emailLower, otherwise false.

Comment on lines +135 to +150
return {
id: event.id || "",
title: event.subject || "Untitled",
description: event.bodyPreview || undefined,
location: event.location?.displayName || undefined,
eventUrl: event.webLink || undefined,
videoConferenceLink:
event.onlineMeeting?.joinUrl || event.onlineMeetingUrl || undefined,
startTime: new Date(event.start?.dateTime || Date.now()),
endTime: new Date(event.end?.dateTime || Date.now()),
attendees:
event.attendees?.map((attendee) => ({
email: attendee.emailAddress?.address || "",
name: attendee.emailAddress?.name ?? undefined,
})) || [],
};
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 | 🟠 Major

Risky fallback values may mask data issues.

Using Date.now() as the fallback for missing event dates (lines 143-144) could create events with incorrect timestamps, making them appear to start/end "now" rather than indicating a data problem. Similarly, returning an empty string for a missing event ID (line 136) may violate downstream expectations that IDs are non-empty.

Consider throwing an error or logging a warning when required fields are missing, or return null/undefined to make the absence explicit.

Apply this diff to make missing required fields explicit:

   return {
-    id: event.id || "",
+    id: event.id ?? "",
     title: event.subject || "Untitled",
     description: event.bodyPreview || undefined,
     location: event.location?.displayName || undefined,
     eventUrl: event.webLink || undefined,
     videoConferenceLink:
       event.onlineMeeting?.joinUrl || event.onlineMeetingUrl || undefined,
-    startTime: new Date(event.start?.dateTime || Date.now()),
-    endTime: new Date(event.end?.dateTime || Date.now()),
+    startTime: event.start?.dateTime ? new Date(event.start.dateTime) : new Date(),
+    endTime: event.end?.dateTime ? new Date(event.end.dateTime) : new Date(),
     attendees:
       event.attendees?.map((attendee) => ({
         email: attendee.emailAddress?.address || "",
         name: attendee.emailAddress?.name ?? undefined,
       })) || [],
   };

Additionally, consider adding validation or logging:

if (!event.id || !event.start?.dateTime || !event.end?.dateTime) {
  logger.warn("Event missing required fields", { 
    eventId: event.id,
    hasStart: !!event.start?.dateTime,
    hasEnd: !!event.end?.dateTime 
  });
}
🤖 Prompt for AI Agents
In apps/web/utils/calendar/providers/microsoft-events.ts around lines 135 to
150, the mapper currently uses Date.now() for missing start/end times and an
empty string for a missing id which can hide data problems; change the mapping
to return undefined (or null) for id, startTime, and endTime when those fields
are absent instead of defaulting to current time/empty string, and add a simple
validation block that logs a warning (using the module logger) when event.id,
event.start?.dateTime, or event.end?.dateTime are missing so the absence is
explicit; optionally, if downstream requires non-null ids, throw a clear error
when id is missing rather than returning a placeholder.

Comment on lines +911 to +947
async getThreadsWithParticipant(options: {
participantEmail: string;
maxThreads?: number;
}): Promise<EmailThread[]> {
const { participantEmail, maxThreads = 5 } = options;

const query = `from:${participantEmail} OR to:${participantEmail}`;
const { threads: gmailThreads } = await getThreadsWithNextPageToken({
gmail: this.client,
q: query,
maxResults: maxThreads,
});

const threadIds = gmailThreads
.map((t) => t.id)
.filter((id): id is string => !!id);

if (threadIds.length === 0) {
return [];
}

const threads = await getThreadsBatch(
threadIds,
getAccessTokenFromClient(this.client),
);

return threads
.filter((thread) => !!thread.id)
.map((thread) => ({
id: thread.id!,
messages:
thread.messages?.map((message) =>
parseMessage(message as MessageWithPayload),
) || [],
snippet: decodeSnippet(thread.snippet),
}));
}
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 | 🟠 Major

Expand participant query to include cc: and normalize email input

The new getThreadsWithParticipant works end‑to‑end but has two correctness gaps:

  1. It only searches from: and to:, so threads where the participant only appears in cc: are silently missed.
  2. It uses participantEmail verbatim. If callers pass values like "Name <user@example.com>", Gmail search will be less reliable than using the normalized address (you already use extractEmailAddress in other helpers).

You can address both with a small refactor:

-  async getThreadsWithParticipant(options: {
-    participantEmail: string;
-    maxThreads?: number;
-  }): Promise<EmailThread[]> {
-    const { participantEmail, maxThreads = 5 } = options;
-
-    const query = `from:${participantEmail} OR to:${participantEmail}`;
+  async getThreadsWithParticipant(options: {
+    participantEmail: string;
+    maxThreads?: number;
+  }): Promise<EmailThread[]> {
+    const { participantEmail, maxThreads = 5 } = options;
+
+    const email =
+      extractEmailAddress(participantEmail) || participantEmail.trim();
+    if (!email) {
+      return [];
+    }
+
+    // Treat “participant” consistently across providers: match from, to, and cc.
+    const query = `from:${email} OR to:${email} OR cc:${email}`;
@@
-    const threads = await getThreadsBatch(
+    const threads = await getThreadsBatch(
       threadIds,
       getAccessTokenFromClient(this.client),
     );
 
-    return threads
-      .filter((thread) => !!thread.id)
-      .map((thread) => ({
-        id: thread.id!,
-        messages:
-          thread.messages?.map((message) =>
-            parseMessage(message as MessageWithPayload),
-          ) || [],
-        snippet: decodeSnippet(thread.snippet),
-      }));
+    return threads
+      .filter((thread) => !!thread.id)
+      .map((thread) => ({
+        id: thread.id!,
+        messages:
+          thread.messages?.map((message) =>
+            parseMessage(message as MessageWithPayload),
+          ) || [],
+        snippet: decodeSnippet(thread.snippet),
+        // Optional: preserve historyId for parity with other thread helpers.
+        historyId: thread.historyId || undefined,
+      }));
   }

This brings Gmail’s behavior in line with Outlook’s “participant” semantics and hardens the method against non‑raw email inputs.

🤖 Prompt for AI Agents
In apps/web/utils/email/google.ts around lines 911 to 947, the participant
search only uses "from:" and "to:" and passes participantEmail verbatim; update
the logic to first normalize the input with
extractEmailAddress(participantEmail) (fall back to original if extraction
fails), then build the Gmail query to include cc: as well (e.g., `from:... OR
to:... OR cc:...`) so threads where the participant appears only in CC are
returned and searches tolerate "Name <user@example.com>" formats.

@@ -61,6 +67,11 @@ export async function deleteUser({ userId }: { userId: string }) {
captureException(error, { extra: { userId } }, userId);
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 | 🟡 Minor

Third parameter to captureException should be an email address, not userId.

Looking at the captureException signature in apps/web/utils/error.ts (lines 35-47), the third parameter is userEmail?: string which is used to call setUser({ email: userEmail }). Passing userId here is semantically incorrect and may cause confusion in Sentry user identification.

-      captureException(error, { extra: { userId } }, userId);
+      captureException(error, { extra: { userId } });

The same issue appears on line 72 and line 97. Consider removing the third argument or passing an actual email address if available.

📝 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
captureException(error, { extra: { userId } }, userId);
captureException(error, { extra: { userId } });
🤖 Prompt for AI Agents
In apps/web/utils/user/delete.ts around lines 67, 72 and 97, the third argument
passed to captureException is userId but captureException's third parameter is
userEmail; replace those incorrect userId arguments by either removing the third
parameter entirely or passing the actual user email (if available in scope) so
setUser receives an email string; update all three calls consistently and ensure
extra context still includes userId in the second-parameter extra object.

Comment on lines +13 to +22
export type MeetingBriefingEmailProps = {
baseUrl: string;
emailAccountId: string;
meetingTitle: string;
formattedTime: string; // e.g., "2:00 PM"
videoConferenceLink?: string;
eventUrl: string;
briefingContent: string;
unsubscribeToken: string;
};
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 | 🟡 Minor

unsubscribeToken prop is declared but never used.

The unsubscribeToken is defined in MeetingBriefingEmailProps but not used in the component. Either use it to generate an unsubscribe link or remove it from the type.

 export type MeetingBriefingEmailProps = {
   baseUrl: string;
   emailAccountId: string;
   meetingTitle: string;
   formattedTime: string; // e.g., "2:00 PM"
   videoConferenceLink?: string;
   eventUrl: string;
   briefingContent: string;
-  unsubscribeToken: string;
 };
📝 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
export type MeetingBriefingEmailProps = {
baseUrl: string;
emailAccountId: string;
meetingTitle: string;
formattedTime: string; // e.g., "2:00 PM"
videoConferenceLink?: string;
eventUrl: string;
briefingContent: string;
unsubscribeToken: string;
};
export type MeetingBriefingEmailProps = {
baseUrl: string;
emailAccountId: string;
meetingTitle: string;
formattedTime: string; // e.g., "2:00 PM"
videoConferenceLink?: string;
eventUrl: string;
briefingContent: string;
};
🤖 Prompt for AI Agents
In packages/resend/emails/meeting-briefing.tsx around lines 13 to 22, the
unsubscribeToken prop is declared in MeetingBriefingEmailProps but never used;
either remove it from the type if not needed, or use it to build an unsubscribe
URL (e.g., `${baseUrl}/unsubscribe?token=${unsubscribeToken}`) and render that
link in the email (preferably in the footer with accessible link text), ensuring
to URL-encode the token and update any callers to pass or stop passing the token
accordingly.

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: 0

♻️ Duplicate comments (1)
apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx (1)

350-360: Fix global error list to handle nested error objects safely

The global error banner still maps Object.values(errors) and assumes each value has a .message, which isn’t true for nested error objects (e.g., actions, conditions). This can render blank list items and duplicate/undefined keys.

Adapting the earlier suggestion, you can map to messages first, filter falsy, and key by index+message:

-              description={
-                <ul className="list-disc">
-                  {Object.values(errors).map((error) => (
-                    <li key={error.message}>{error.message}</li>
-                  ))}
-                </ul>
-              }
+              description={
+                <ul className="list-disc">
+                  {Object.values(errors)
+                    .map((e: any) => e?.message as string | undefined)
+                    .filter((msg): msg is string => !!msg)
+                    .map((msg, i) => (
+                      <li key={`${i}-${msg}`}>{msg}</li>
+                    ))}
+                </ul>
+              }

This at least prevents empty entries and key collisions for top‑level field errors.

🧹 Nitpick comments (7)
apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx (2)

341-357: Prefer shared Input component instead of raw TextareaAutosize for instructions

The instructions field uses TextareaAutosize directly; elsewhere the app standardizes form fields through the Input component (including autosizing textareas), which centralizes styling and error handling. Consider switching this to Input with autosizeTextarea to keep form UX consistent with the rest of the app.
Based on learnings, this keeps text area handling consistent.


477-478: Tighten tooltip copy for “from/to” filters

Only apply this rule ${filterType} emails from this address reads oddly for the "from" case (“from emails from this address”). Consider wording like Only apply this rule to emails ${filterType} this address… to avoid repetition and be clearer.

apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx (3)

4-10: Simplify React Hook Form typings instead of using ReturnType<typeof useForm>

You’re importing useForm as a type and then using ReturnType<typeof useForm<CreateRuleBody>>["..."] to type register, watch, setValue, and control. This is harder to read and ties the types directly to the hook implementation.

Since you already use UseFormRegister, UseFormWatch, UseFormSetValue, and Control in ActionSteps, you can reuse those here (and optionally UseFormReturn) for consistency:

-import type {
-  useForm,
-  Control,
-  UseFormRegister,
-  UseFormSetValue,
-  UseFormWatch,
-} from "react-hook-form";
+import type {
+  Control,
+  UseFormRegister,
+  UseFormSetValue,
+  UseFormWatch,
+  UseFormReturn,
+} from "react-hook-form";
...
 function ActionCard({
   index,
   register,
   watch,
   setValue,
   control,
@@
-}: {
-  action: CreateRuleBody["actions"][number];
-  index: number;
-  register: ReturnType<typeof useForm<CreateRuleBody>>["register"];
-  watch: ReturnType<typeof useForm<CreateRuleBody>>["watch"];
-  setValue: ReturnType<typeof useForm<CreateRuleBody>>["setValue"];
-  control: ReturnType<typeof useForm<CreateRuleBody>>["control"];
+}: {
+  action: CreateRuleBody["actions"][number];
+  index: number;
+  register: UseFormRegister<CreateRuleBody>;
+  watch: UseFormWatch<CreateRuleBody>;
+  setValue: UseFormSetValue<CreateRuleBody>;
+  control: Control<CreateRuleBody>;
@@
-function DelayInputControls({
+function DelayInputControls({
   index,
   delayInMinutes,
   setValue,
 }: {
   index: number;
   delayInMinutes: number | null | undefined;
-  setValue: ReturnType<typeof useForm<CreateRuleBody>>["setValue"];
+  setValue: UseFormReturn<CreateRuleBody>["setValue"];
 }) {

This keeps the component aligned with the rest of the form code and avoids subtle confusion around import type and typeof useForm.

Also applies to: 124-131, 672-680


109-140: Remove unused action prop to avoid lint noise

ActionCard accepts an action prop but never uses it inside the function. You can drop it from both the prop list and the call site to satisfy no‑unused‑parameters rules and keep the API tighter.


394-399: Route email/content textareas through the shared Input component

The email content fields are rendered with TextareaAutosize directly. Given the project convention of using the shared Input component (with autosizeTextarea for multi-line text) for form fields, it would be better to wrap these textareas in Input as well for consistent styling, validation, and future behavior changes.

Refactor these to Input with autosizeTextarea, rows, and registerProps matching the existing pattern.
Based on learnings, this aligns text area handling with other forms.

apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx (2)

191-194: Avoid console.error in form submit error handling

The submit handler logs console.error(res) on server errors. Project guidelines discourage direct console usage; you’re already surfacing the problem via toastError, so the console log is mostly redundant.

Consider removing the console.error or routing it through your centralized logging/monitoring instead.


614-619: Deduplicate allowMultipleConditions between RuleForm and ConditionSteps

allowMultipleConditions is defined here and again in ConditionSteps.tsx with identical logic. Centralizing this helper (e.g., in a small shared utility under utils/rule/) would avoid divergence if the rules for multi-condition support change later.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6067f44 and 5a0ebfc.

📒 Files selected for processing (6)
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx (1 hunks)
  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx (1 hunks)
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx (7 hunks)
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx (1 hunks)
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx (1 hunks)
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSteps.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
🧰 Additional context used
📓 Path-based instructions (14)
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/web/CLAUDE.md)

apps/web/**/*.{ts,tsx}: Use TypeScript with strict null checks
Use @/ path aliases for imports from project root
Use proper error handling with try/catch blocks
Format code with Prettier
Follow consistent naming conventions using PascalCase for components
Centralize shared types in dedicated type files

Import specific lodash functions rather than entire lodash library to minimize bundle size (e.g., import groupBy from 'lodash/groupBy')

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
apps/web/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/web/CLAUDE.md)

Follow NextJS app router structure with (app) directory

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
apps/web/**/*.tsx

📄 CodeRabbit inference engine (apps/web/CLAUDE.md)

apps/web/**/*.tsx: Follow tailwindcss patterns with prettier-plugin-tailwindcss for class sorting
Prefer functional components with hooks over class components
Use shadcn/ui components when available
Ensure responsive design with mobile-first approach
Use LoadingContent component for async data with loading and error states

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/data-fetching.mdc)

**/*.{ts,tsx}: For API GET requests to server, use the swr package
Use result?.serverError with toastError from @/components/Toast for error handling in async operations

**/*.{ts,tsx}: Use wrapper functions for Gmail message operations (get, list, batch, etc.) from @/utils/gmail/message.ts instead of direct API calls
Use wrapper functions for Gmail thread operations from @/utils/gmail/thread.ts instead of direct API calls
Use wrapper functions for Gmail label operations from @/utils/gmail/label.ts instead of direct API calls

**/*.{ts,tsx}: For early access feature flags, create hooks using the naming convention use[FeatureName]Enabled that return a boolean from useFeatureFlagEnabled("flag-key")
For A/B test variant flags, create hooks using the naming convention use[FeatureName]Variant that define variant types, use useFeatureFlagVariantKey() with type casting, and provide a default "control" fallback
Use kebab-case for PostHog feature flag keys (e.g., inbox-cleaner, pricing-options-2)
Always define types for A/B test variant flags (e.g., type PricingVariant = "control" | "variant-a" | "variant-b") and provide type safety through type casting

**/*.{ts,tsx}: Don't use primitive type aliases or misleading types
Don't use empty type parameters in type aliases and interfaces
Don't use this and super in static contexts
Don't use any or unknown as type constraints
Don't use the TypeScript directive @ts-ignore
Don't use TypeScript enums
Don't export imported variables
Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions
Don't use TypeScript namespaces
Don't use non-null assertions with the ! postfix operator
Don't use parameter properties in class constructors
Don't use user-defined types
Use as const instead of literal types and type annotations
Use either T[] or Array<T> consistently
Initialize each enum member value explicitly
Use export type for types
Use `impo...

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
apps/web/app/(app)/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/page-structure.mdc)

apps/web/app/(app)/**/*.{ts,tsx}: Components for the page are either put in page.tsx, or in the apps/web/app/(app)/PAGE_NAME folder
If we're in a deeply nested component we will use swr to fetch via API
If you need to use onClick in a component, that component is a client component and file must start with use client

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/prisma-enum-imports.mdc)

Always import Prisma enums from @/generated/prisma/enums instead of @/generated/prisma/client to avoid Next.js bundling errors in client components

Import Prisma using the project's centralized utility: import prisma from '@/utils/prisma'

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
**/*.{tsx,ts}

📄 CodeRabbit inference engine (.cursor/rules/ui-components.mdc)

**/*.{tsx,ts}: Use Shadcn UI and Tailwind for components and styling
Use next/image package for images
For API GET requests to server, use the swr package with hooks like useSWR to fetch data
For text inputs, use the Input component with registerProps for form integration and error handling

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
**/*.{tsx,ts,css}

📄 CodeRabbit inference engine (.cursor/rules/ui-components.mdc)

Implement responsive design with Tailwind CSS using a mobile-first approach

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/ui-components.mdc)

**/*.tsx: Use the LoadingContent component to handle loading states instead of manual loading state management
For text areas, use the Input component with type='text', autosizeTextarea prop set to true, and registerProps for form integration

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{js,jsx,ts,tsx}: Don't use accessKey attribute on any HTML element
Don't set aria-hidden="true" on focusable elements
Don't add ARIA roles, states, and properties to elements that don't support them
Don't use distracting elements like <marquee> or <blink>
Only use the scope prop on <th> elements
Don't assign non-interactive ARIA roles to interactive HTML elements
Make sure label elements have text content and are associated with an input
Don't assign interactive ARIA roles to non-interactive HTML elements
Don't assign tabIndex to non-interactive HTML elements
Don't use positive integers for tabIndex property
Don't include "image", "picture", or "photo" in img alt prop
Don't use explicit role property that's the same as the implicit/default role
Make static elements with click handlers use a valid role attribute
Always include a title element for SVG elements
Give all elements requiring alt text meaningful information for screen readers
Make sure anchors have content that's accessible to screen readers
Assign tabIndex to non-interactive HTML elements with aria-activedescendant
Include all required ARIA attributes for elements with ARIA roles
Make sure ARIA properties are valid for the element's supported roles
Always include a type attribute for button elements
Make elements with interactive roles and handlers focusable
Give heading elements content that's accessible to screen readers (not hidden with aria-hidden)
Always include a lang attribute on the html element
Always include a title attribute for iframe elements
Accompany onClick with at least one of: onKeyUp, onKeyDown, or onKeyPress
Accompany onMouseOver/onMouseOut with onFocus/onBlur
Include caption tracks for audio and video elements
Use semantic elements instead of role attributes in JSX
Make sure all anchors are valid and navigable
Ensure all ARIA properties (aria-*) are valid
Use valid, non-abstract ARIA roles for elements with ARIA roles
Use valid AR...

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{jsx,tsx}: Don't use unnecessary fragments
Don't pass children as props
Don't use the return value of React.render
Make sure all dependencies are correctly specified in React hooks
Make sure all React hooks are called from the top level of component functions
Don't forget key props in iterators and collection literals
Don't define React components inside other components
Don't use event handlers on non-interactive elements
Don't assign to React component props
Don't use both children and dangerouslySetInnerHTML props on the same element
Don't use dangerous JSX props
Don't use Array index in keys
Don't insert comments as text nodes
Don't assign JSX properties multiple times
Don't add extra closing tags for components without children
Use <>...</> instead of <Fragment>...</Fragment>
Watch out for possible "wrong" semicolons inside JSX elements
Make sure void (self-closing) elements don't have children
Don't use target="_blank" without rel="noopener"
Don't use <img> elements in Next.js projects
Don't use <head> elements in Next.js projects

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
!(pages/_document).{jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

Don't use the next/head module in pages/_document.js on Next.js projects

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/utilities.mdc)

**/*.{js,ts,jsx,tsx}: Use lodash utilities for common operations (arrays, objects, strings)
Import specific lodash functions to minimize bundle size (e.g., import groupBy from 'lodash/groupBy')

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
**/*Form.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/form-handling.mdc)

**/*Form.{ts,tsx}: Use React Hook Form with Zod for validation in form components
Validate form inputs before submission
Show validation errors inline next to form fields

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
🧠 Learnings (28)
📚 Learning: 2025-11-25T14:40:13.649Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ui-components.mdc:0-0
Timestamp: 2025-11-25T14:40:13.649Z
Learning: Applies to **/*.{tsx,ts,jsx,js} : For text inputs in forms, use the `Input` component with `type='email'`, `name`, `label`, `registerProps` from react-hook-form, and `error` props

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:51.389Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-11-25T14:36:51.389Z
Learning: Applies to **/*Form.{ts,tsx} : Show validation errors inline next to form fields in the UI

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:38:56.992Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/project-structure.mdc:0-0
Timestamp: 2025-11-25T14:38:56.992Z
Learning: Applies to apps/web/components/ui/**/*.tsx : Shadcn UI components are located in `components/ui` directory

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:39:49.448Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/server-actions.mdc:0-0
Timestamp: 2025-11-25T14:39:49.448Z
Learning: Applies to apps/web/utils/actions/*.ts : Create corresponding server action implementation files using the naming convention `apps/web/utils/actions/NAME.ts` with 'use server' directive

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
📚 Learning: 2025-11-25T14:39:49.448Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/server-actions.mdc:0-0
Timestamp: 2025-11-25T14:39:49.448Z
Learning: Applies to apps/web/utils/actions/*.ts : Add metadata with a meaningful action name using `.metadata({ name: "actionName" })` for Sentry instrumentation and monitoring

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
📚 Learning: 2025-11-25T14:40:13.649Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ui-components.mdc:0-0
Timestamp: 2025-11-25T14:40:13.649Z
Learning: Applies to **/*.{tsx,ts,jsx,js} : For text areas in forms, use the `Input` component with `type='text'`, `autosizeTextarea` prop, `rows`, `name`, `placeholder`, `registerProps` from react-hook-form, and `error` props

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:38:07.606Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/llm.mdc:0-0
Timestamp: 2025-11-25T14:38:07.606Z
Learning: Applies to apps/web/utils/ai/**/*.ts : Use XML-like tags to structure data in prompts, remove excessive whitespace and truncate long inputs, and format data consistently across similar LLM functions

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:38:07.606Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/llm.mdc:0-0
Timestamp: 2025-11-25T14:38:07.606Z
Learning: Applies to apps/web/utils/ai/**/*.ts : LLM feature functions must follow a standard structure: accept options with `inputData` and `emailAccount` parameters, implement input validation with early returns, define separate system and user prompts, create a Zod schema for response validation, and use `createGenerateObject` to execute the LLM call

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:39:23.326Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/security.mdc:0-0
Timestamp: 2025-11-25T14:39:23.326Z
Learning: Applies to app/api/**/*.ts : Use `SafeError` for error responses to prevent information disclosure - provide generic messages (e.g., 'Rule not found' not 'Rule {id} does not exist for user {userId}') without revealing internal IDs or ownership details

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:53.147Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-11-25T14:36:53.147Z
Learning: Applies to **/*Form.{ts,tsx} : Show validation errors inline next to form fields

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:53.147Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-11-25T14:36:53.147Z
Learning: Applies to **/*.validation.{ts,tsx} : Use descriptive error messages in validation schemas

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:18.416Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: apps/web/CLAUDE.md:0-0
Timestamp: 2025-11-25T14:36:18.416Z
Learning: Applies to apps/web/components/**/*.tsx : Use `result?.serverError` with `toastError` and `toastSuccess` for error handling in form submissions

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:42:08.869Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2025-11-25T14:42:08.869Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Make sure to pass a message value when creating a built-in error

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:51.389Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-11-25T14:36:51.389Z
Learning: Applies to **/*.validation.ts : Use descriptive error messages in Zod validation schemas

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-07-17T04:19:57.099Z
Learnt from: edulelis
Repo: elie222/inbox-zero PR: 576
File: packages/resend/emails/digest.tsx:78-83
Timestamp: 2025-07-17T04:19:57.099Z
Learning: In packages/resend/emails/digest.tsx, the DigestEmailProps type uses `[key: string]: DigestItem[] | undefined | string | Date | undefined` instead of intersection types like `& Record<string, DigestItem[] | undefined>` due to implementation constraints. This was the initial implementation approach and cannot be changed to more restrictive typing.

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:37:09.306Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/fullstack-workflow.mdc:0-0
Timestamp: 2025-11-25T14:37:09.306Z
Learning: Applies to apps/web/components/**/*Form*.tsx : Handle form submission results using `result?.serverError` to show error toasts and `toastSuccess` to show success messages after server action completion

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:39:27.909Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/security.mdc:0-0
Timestamp: 2025-11-25T14:39:27.909Z
Learning: Applies to **/*.ts : Use SafeError for error responses to prevent information disclosure. Generic error messages should not reveal internal IDs, logic, or resource ownership details

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:51.389Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-11-25T14:36:51.389Z
Learning: Applies to **/*Form.{ts,tsx} : Validate form inputs before submission using React Hook Form and Zod resolver

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:37:09.306Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/fullstack-workflow.mdc:0-0
Timestamp: 2025-11-25T14:37:09.306Z
Learning: Applies to apps/web/components/**/*Form*.tsx : Use React Hook Form with Zod validation (`zodResolver`) for form handling, with form components using `register`, `handleSubmit`, and error handling from the hook

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:51.389Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-11-25T14:36:51.389Z
Learning: Applies to **/*Form.{ts,tsx} : Use React Hook Form with Zod for form validation

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:18.416Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: apps/web/CLAUDE.md:0-0
Timestamp: 2025-11-25T14:36:18.416Z
Learning: Applies to apps/web/components/**/*.tsx : Use React Hook Form with Zod validation for form components

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:53.147Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-11-25T14:36:53.147Z
Learning: Applies to **/*Form.{ts,tsx} : Use React Hook Form with Zod for validation in form components

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:53.147Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-11-25T14:36:53.147Z
Learning: Applies to **/*Form.{ts,tsx} : Validate form inputs before submission

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:18.416Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: apps/web/CLAUDE.md:0-0
Timestamp: 2025-11-25T14:36:18.416Z
Learning: Applies to apps/web/**/*.tsx : Use shadcn/ui components when available

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:42:11.919Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/utilities.mdc:0-0
Timestamp: 2025-11-25T14:42:11.919Z
Learning: Applies to utils/**/*.{js,ts,jsx,tsx} : The `utils` folder contains core app logic such as Next.js Server Actions and Gmail API requests

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:40:00.833Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-11-25T14:40:00.833Z
Learning: Applies to **/*.test.{ts,tsx} : Use test helpers `getEmail`, `getEmailAccount`, and `getRule` from `@/__tests__/helpers` for mocking emails, accounts, and rules

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:40:15.063Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ui-components.mdc:0-0
Timestamp: 2025-11-25T14:40:15.063Z
Learning: Applies to **/*.{tsx,ts} : For text inputs, use the `Input` component with `registerProps` for form integration and error handling

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:40:15.063Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ui-components.mdc:0-0
Timestamp: 2025-11-25T14:40:15.063Z
Learning: Applies to **/*.tsx : For text areas, use the `Input` component with `type='text'`, `autosizeTextarea` prop set to true, and `registerProps` for form integration

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
⏰ 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: test
🔇 Additional comments (2)
apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx (1)

5-57: Section wrapper abstraction looks clean and reusable

The RuleSectionCard API and layout are clear and composable, and it cleanly separates header, errors, body, and footer actions. This should make the rule editor sections much easier to maintain.

apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx (1)

142-158: Guard against undefined actionType when indexing actionInputs

actionType is read from watch("actions.${index}.type") and then used as actionInputs[actionType].fields. If, for any reason, the form state has an undefined or transiently empty type, this will throw at runtime.

Consider providing a safe fallback and optional chaining:

-  const actionType = watch(`actions.${index}.type`);
-  const fields = actionInputs[actionType].fields;
+  const actionType = watch(`actions.${index}.type`) ?? ActionType.LABEL;
+  const fields = actionInputs[actionType]?.fields ?? [];

This keeps the UI resilient even if the form state is temporarily incomplete.

Also applies to: 266-268, 269-271

⛔ Skipped due to learnings
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2025-11-25T14:42:08.869Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Don't use optional chaining where undefined values aren't allowed
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2025-11-25T14:42:08.869Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Make sure label elements have text content and are associated with an input
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ui-components.mdc:0-0
Timestamp: 2025-11-25T14:40:13.649Z
Learning: Applies to **/*.{tsx,ts,jsx,js} : For text inputs in forms, use the `Input` component with `type='email'`, `name`, `label`, `registerProps` from react-hook-form, and `error` props
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2025-11-25T14:42:08.869Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Make sure typeof expressions are compared to valid values
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2025-11-25T14:42:08.869Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Use valid ARIA state and property values
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: apps/web/CLAUDE.md:0-0
Timestamp: 2025-11-25T14:36:18.416Z
Learning: Applies to apps/web/utils/actions/**/*.ts : Use `next-safe-action` with `actionClient` for server actions with Zod schema validation
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2025-11-25T14:42:08.869Z
Learning: Applies to **/*.{jsx,tsx} : Don't use Array index in keys
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ui-components.mdc:0-0
Timestamp: 2025-11-25T14:40:13.649Z
Learning: Applies to **/*.{tsx,ts,jsx,js} : For text areas in forms, use the `Input` component with `type='text'`, `autosizeTextarea` prop, `rows`, `name`, `placeholder`, `registerProps` from react-hook-form, and `error` props
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2025-11-25T14:42:08.869Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Use `at()` instead of integer index access

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: 1

♻️ Duplicate comments (1)
apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx (1)

350-360: Global error summary can render blank items and duplicate/undefined keys

Object.values(errors).map((error) => (… key={error.message} …)) assumes every entry is a flat FieldError with a message. In practice, errors can contain nested objects/arrays, so you can end up with:

  • <li> items with no visible text (error.message undefined)
  • Duplicate or undefined React keys

Consider mapping to messages, filtering falsy, and keying by index + message, in line with the prior Macroscope suggestion:

-              description={
-                <ul className="list-disc">
-                  {Object.values(errors).map((error) => (
-                    <li key={error.message}>{error.message}</li>
-                  ))}
-                </ul>
-              }
+              description={
+                <ul className="list-disc">
+                  {Object.values(errors)
+                    .map((error: any) => error?.message)
+                    .filter((msg): msg is string => !!msg)
+                    .map((msg, index) => (
+                      <li key={`${index}-${msg}`}>{msg}</li>
+                    ))}
+                </ul>
+              }
🧹 Nitpick comments (6)
apps/web/components/LoadingContent.tsx (2)

36-36: Remove redundant type assertion.

The error parameter is already typed as LoadingContentProps["error"], which includes status?: number. The type assertion is unnecessary.

-  const status = (error as { status?: number })?.status;
+  const status = error?.status;

8-8: Consider consolidating the error type structure.

The error type has a complex structure with two paths for error messages (info?.error and error) plus the new status field. This works but could be simplified for better maintainability.

Consider defining a dedicated error type:

interface LoadingError {
  message: string;
  status?: number;
  info?: { error: string };
}

// Then use in LoadingContentProps:
error?: LoadingError;
apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx (2)

191-195: Avoid console.error in submit handlers; rely on toasts / logging utilities

The console.error(res); calls in the update/create branches conflict with the “Don’t use console” guideline and can leak noisy output in production. You’re already surfacing failures via toastError, so logging here is optional; if you still want logs, consider routing through a centralized logger instead of console.

-        if (res?.serverError) {
-          console.error(res);
-          toastError({ description: res.serverError });
+        if (res?.serverError) {
+          toastError({ description: res.serverError });

And similarly for the create branch.

Also applies to: 222-224


386-425: New When/Then section cards and Advanced Settings dialog are a nice modularization (small nits only)

The refactor to RuleSectionCard + ConditionSteps / ActionSteps, plus the Advanced Settings dialog (runOnThreads, digest, LearnedPatterns, delete flow, cancel/save buttons), reads clean and keeps concerns separated while still wiring correctly into the form state and validation. This aligns well with the form-handling and UI guidelines (modular steps, inline errors, shadcn components). Two tiny nits you can consider:

  • You can omit headerActions={undefined} / footerActions={undefined} props on RuleSectionCard to trim noise; undefined is the default.
  • The inline onClick async handler for the delete button is somewhat long; extracting it into a useCallback (e.g. handleDeleteRule) would slightly improve readability and testability, but it’s not required.

No functional changes required here; these are optional cleanups.

Also applies to: 427-466, 468-571, 576-599, 604-619

apps/web/providers/SWRProvider.tsx (2)

141-150: Avoid console.log in SWR onError and wire this into central error reporting

The new getDevOnlySWRConfig integration looks good, but onError still logs directly to the console, which conflicts with the repo guideline to avoid console. Given you already have captureException wired above, consider routing SWR errors through it instead (and, if you still want console output, gating that behind NODE_ENV === "development"):

-          // TODO: Send to Sentry
-          onError: (error) => console.log("SWR error:", error),
+          // TODO: Adjust tagging/metadata as needed, but avoid raw console logging.
+          onError: (error, key) => {
+            captureException(error, {
+              extra: {
+                source: "swr-onError",
+                key,
+              },
+            });
+          },

This keeps error handling consistent with the rest of the file and removes direct console usage.


158-182: Add an explicit retry limit to prevent unbounded revalidation in development

The concern about unbounded retries is valid—the custom onErrorRetry handler has no cap and will retry indefinitely. However, the idiomatic approach in SWR 2.3.7 is to set errorRetryCount in the config rather than checking retryCount inside the callback:

  return {
    keepPreviousData: true,
+   errorRetryCount: 5,
    onErrorRetry: (
      error: Error & { status?: number },
      _key: string,
      _config: unknown,
      revalidate: (opts: { retryCount: number }) => void,
      { retryCount }: { retryCount: number },
    ) => {
      // Retry 404s quickly (likely HMR transient errors)
      if (error.status === 404) {
        setTimeout(() => revalidate({ retryCount }), 500);
        return;
      }
      // Don't retry on other client errors (4xx)
      if (error.status && error.status >= 400 && error.status < 500) return;
      // Default exponential backoff for server errors
      setTimeout(() => revalidate({ retryCount }), 5000 * 2 ** retryCount);
    },
  };

This lets SWR handle the retry limit automatically rather than relying on manual checks. You could optionally type the function as (): Partial<SWRConfiguration> for editor support.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5a0ebfc and 500fcac.

📒 Files selected for processing (4)
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx (7 hunks)
  • apps/web/components/LoadingContent.tsx (2 hunks)
  • apps/web/providers/SWRProvider.tsx (3 hunks)
  • version.txt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • version.txt
🧰 Additional context used
📓 Path-based instructions (16)
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/web/CLAUDE.md)

apps/web/**/*.{ts,tsx}: Use TypeScript with strict null checks
Use @/ path aliases for imports from project root
Use proper error handling with try/catch blocks
Format code with Prettier
Follow consistent naming conventions using PascalCase for components
Centralize shared types in dedicated type files

Import specific lodash functions rather than entire lodash library to minimize bundle size (e.g., import groupBy from 'lodash/groupBy')

Files:

  • apps/web/components/LoadingContent.tsx
  • apps/web/providers/SWRProvider.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
apps/web/**/*.tsx

📄 CodeRabbit inference engine (apps/web/CLAUDE.md)

apps/web/**/*.tsx: Follow tailwindcss patterns with prettier-plugin-tailwindcss for class sorting
Prefer functional components with hooks over class components
Use shadcn/ui components when available
Ensure responsive design with mobile-first approach
Use LoadingContent component for async data with loading and error states

Files:

  • apps/web/components/LoadingContent.tsx
  • apps/web/providers/SWRProvider.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
apps/web/components/**/*.tsx

📄 CodeRabbit inference engine (apps/web/CLAUDE.md)

apps/web/components/**/*.tsx: Use React Hook Form with Zod validation for form components
Use result?.serverError with toastError and toastSuccess for error handling in form submissions

Use LoadingContent component to consistently handle loading and error states, passing loading, error, and children props

Use PascalCase for component file names (e.g., components/Button.tsx)

Files:

  • apps/web/components/LoadingContent.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/data-fetching.mdc)

**/*.{ts,tsx}: For API GET requests to server, use the swr package
Use result?.serverError with toastError from @/components/Toast for error handling in async operations

**/*.{ts,tsx}: Use wrapper functions for Gmail message operations (get, list, batch, etc.) from @/utils/gmail/message.ts instead of direct API calls
Use wrapper functions for Gmail thread operations from @/utils/gmail/thread.ts instead of direct API calls
Use wrapper functions for Gmail label operations from @/utils/gmail/label.ts instead of direct API calls

**/*.{ts,tsx}: For early access feature flags, create hooks using the naming convention use[FeatureName]Enabled that return a boolean from useFeatureFlagEnabled("flag-key")
For A/B test variant flags, create hooks using the naming convention use[FeatureName]Variant that define variant types, use useFeatureFlagVariantKey() with type casting, and provide a default "control" fallback
Use kebab-case for PostHog feature flag keys (e.g., inbox-cleaner, pricing-options-2)
Always define types for A/B test variant flags (e.g., type PricingVariant = "control" | "variant-a" | "variant-b") and provide type safety through type casting

**/*.{ts,tsx}: Don't use primitive type aliases or misleading types
Don't use empty type parameters in type aliases and interfaces
Don't use this and super in static contexts
Don't use any or unknown as type constraints
Don't use the TypeScript directive @ts-ignore
Don't use TypeScript enums
Don't export imported variables
Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions
Don't use TypeScript namespaces
Don't use non-null assertions with the ! postfix operator
Don't use parameter properties in class constructors
Don't use user-defined types
Use as const instead of literal types and type annotations
Use either T[] or Array<T> consistently
Initialize each enum member value explicitly
Use export type for types
Use `impo...

Files:

  • apps/web/components/LoadingContent.tsx
  • apps/web/providers/SWRProvider.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
**/{pages,routes,components}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/gmail-api.mdc)

Never call Gmail API directly from routes or components - always use wrapper functions from the utils folder

Files:

  • apps/web/components/LoadingContent.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/prisma-enum-imports.mdc)

Always import Prisma enums from @/generated/prisma/enums instead of @/generated/prisma/client to avoid Next.js bundling errors in client components

Import Prisma using the project's centralized utility: import prisma from '@/utils/prisma'

Files:

  • apps/web/components/LoadingContent.tsx
  • apps/web/providers/SWRProvider.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
**/*.{tsx,ts}

📄 CodeRabbit inference engine (.cursor/rules/ui-components.mdc)

**/*.{tsx,ts}: Use Shadcn UI and Tailwind for components and styling
Use next/image package for images
For API GET requests to server, use the swr package with hooks like useSWR to fetch data
For text inputs, use the Input component with registerProps for form integration and error handling

Files:

  • apps/web/components/LoadingContent.tsx
  • apps/web/providers/SWRProvider.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
**/*.{tsx,ts,css}

📄 CodeRabbit inference engine (.cursor/rules/ui-components.mdc)

Implement responsive design with Tailwind CSS using a mobile-first approach

Files:

  • apps/web/components/LoadingContent.tsx
  • apps/web/providers/SWRProvider.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/ui-components.mdc)

**/*.tsx: Use the LoadingContent component to handle loading states instead of manual loading state management
For text areas, use the Input component with type='text', autosizeTextarea prop set to true, and registerProps for form integration

Files:

  • apps/web/components/LoadingContent.tsx
  • apps/web/providers/SWRProvider.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{js,jsx,ts,tsx}: Don't use accessKey attribute on any HTML element
Don't set aria-hidden="true" on focusable elements
Don't add ARIA roles, states, and properties to elements that don't support them
Don't use distracting elements like <marquee> or <blink>
Only use the scope prop on <th> elements
Don't assign non-interactive ARIA roles to interactive HTML elements
Make sure label elements have text content and are associated with an input
Don't assign interactive ARIA roles to non-interactive HTML elements
Don't assign tabIndex to non-interactive HTML elements
Don't use positive integers for tabIndex property
Don't include "image", "picture", or "photo" in img alt prop
Don't use explicit role property that's the same as the implicit/default role
Make static elements with click handlers use a valid role attribute
Always include a title element for SVG elements
Give all elements requiring alt text meaningful information for screen readers
Make sure anchors have content that's accessible to screen readers
Assign tabIndex to non-interactive HTML elements with aria-activedescendant
Include all required ARIA attributes for elements with ARIA roles
Make sure ARIA properties are valid for the element's supported roles
Always include a type attribute for button elements
Make elements with interactive roles and handlers focusable
Give heading elements content that's accessible to screen readers (not hidden with aria-hidden)
Always include a lang attribute on the html element
Always include a title attribute for iframe elements
Accompany onClick with at least one of: onKeyUp, onKeyDown, or onKeyPress
Accompany onMouseOver/onMouseOut with onFocus/onBlur
Include caption tracks for audio and video elements
Use semantic elements instead of role attributes in JSX
Make sure all anchors are valid and navigable
Ensure all ARIA properties (aria-*) are valid
Use valid, non-abstract ARIA roles for elements with ARIA roles
Use valid AR...

Files:

  • apps/web/components/LoadingContent.tsx
  • apps/web/providers/SWRProvider.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{jsx,tsx}: Don't use unnecessary fragments
Don't pass children as props
Don't use the return value of React.render
Make sure all dependencies are correctly specified in React hooks
Make sure all React hooks are called from the top level of component functions
Don't forget key props in iterators and collection literals
Don't define React components inside other components
Don't use event handlers on non-interactive elements
Don't assign to React component props
Don't use both children and dangerouslySetInnerHTML props on the same element
Don't use dangerous JSX props
Don't use Array index in keys
Don't insert comments as text nodes
Don't assign JSX properties multiple times
Don't add extra closing tags for components without children
Use <>...</> instead of <Fragment>...</Fragment>
Watch out for possible "wrong" semicolons inside JSX elements
Make sure void (self-closing) elements don't have children
Don't use target="_blank" without rel="noopener"
Don't use <img> elements in Next.js projects
Don't use <head> elements in Next.js projects

Files:

  • apps/web/components/LoadingContent.tsx
  • apps/web/providers/SWRProvider.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
!(pages/_document).{jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

Don't use the next/head module in pages/_document.js on Next.js projects

Files:

  • apps/web/components/LoadingContent.tsx
  • apps/web/providers/SWRProvider.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/utilities.mdc)

**/*.{js,ts,jsx,tsx}: Use lodash utilities for common operations (arrays, objects, strings)
Import specific lodash functions to minimize bundle size (e.g., import groupBy from 'lodash/groupBy')

Files:

  • apps/web/components/LoadingContent.tsx
  • apps/web/providers/SWRProvider.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
apps/web/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/web/CLAUDE.md)

Follow NextJS app router structure with (app) directory

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
**/*Form.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/form-handling.mdc)

**/*Form.{ts,tsx}: Use React Hook Form with Zod for validation in form components
Validate form inputs before submission
Show validation errors inline next to form fields

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
apps/web/app/(app)/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/page-structure.mdc)

apps/web/app/(app)/**/*.{ts,tsx}: Components for the page are either put in page.tsx, or in the apps/web/app/(app)/PAGE_NAME folder
If we're in a deeply nested component we will use swr to fetch via API
If you need to use onClick in a component, that component is a client component and file must start with use client

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
🧠 Learnings (42)
📚 Learning: 2025-11-25T14:37:09.306Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/fullstack-workflow.mdc:0-0
Timestamp: 2025-11-25T14:37:09.306Z
Learning: Applies to apps/web/components/**/*.tsx : Use `LoadingContent` component to consistently handle loading and error states, passing `loading`, `error`, and `children` props

Applied to files:

  • apps/web/components/LoadingContent.tsx
📚 Learning: 2025-11-25T14:36:18.416Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: apps/web/CLAUDE.md:0-0
Timestamp: 2025-11-25T14:36:18.416Z
Learning: Applies to apps/web/**/*.tsx : Use LoadingContent component for async data with loading and error states

Applied to files:

  • apps/web/components/LoadingContent.tsx
📚 Learning: 2025-11-25T14:40:13.649Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ui-components.mdc:0-0
Timestamp: 2025-11-25T14:40:13.649Z
Learning: Applies to **/*.{tsx,ts,jsx,js} : Use the `LoadingContent` component to handle loading states

Applied to files:

  • apps/web/components/LoadingContent.tsx
📚 Learning: 2025-11-25T14:40:15.063Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ui-components.mdc:0-0
Timestamp: 2025-11-25T14:40:15.063Z
Learning: Applies to **/*.tsx : Use the `LoadingContent` component to handle loading states instead of manual loading state management

Applied to files:

  • apps/web/components/LoadingContent.tsx
📚 Learning: 2025-11-25T14:36:40.146Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/data-fetching.mdc:0-0
Timestamp: 2025-11-25T14:36:40.146Z
Learning: Applies to **/*.{ts,tsx} : Use `result?.serverError` with `toastError` from `@/components/Toast` for error handling in async operations

Applied to files:

  • apps/web/components/LoadingContent.tsx
📚 Learning: 2025-11-25T14:38:18.874Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/page-structure.mdc:0-0
Timestamp: 2025-11-25T14:38:18.874Z
Learning: Applies to apps/web/app/(app)/**/*.tsx : If nested deeply in components, use `swr` to fetch via API instead of loading data directly

Applied to files:

  • apps/web/providers/SWRProvider.tsx
📚 Learning: 2025-11-25T14:38:23.265Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/page-structure.mdc:0-0
Timestamp: 2025-11-25T14:38:23.265Z
Learning: Applies to apps/web/app/(app)/**/*.{ts,tsx} : If we're in a deeply nested component we will use `swr` to fetch via API

Applied to files:

  • apps/web/providers/SWRProvider.tsx
📚 Learning: 2025-11-25T14:40:13.649Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ui-components.mdc:0-0
Timestamp: 2025-11-25T14:40:13.649Z
Learning: Applies to **/*.{tsx,ts,jsx,js} : For API get requests to server, use the `swr` package with `useSWR` hook

Applied to files:

  • apps/web/providers/SWRProvider.tsx
📚 Learning: 2025-11-25T14:36:36.276Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/data-fetching.mdc:0-0
Timestamp: 2025-11-25T14:36:36.276Z
Learning: Applies to **/*.{ts,tsx} : For API GET requests to server, use the `swr` package

Applied to files:

  • apps/web/providers/SWRProvider.tsx
📚 Learning: 2025-11-25T14:36:36.276Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/data-fetching.mdc:0-0
Timestamp: 2025-11-25T14:36:36.276Z
Learning: For mutating data, use Next.js server actions instead of SWR

Applied to files:

  • apps/web/providers/SWRProvider.tsx
📚 Learning: 2025-11-25T14:40:15.063Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ui-components.mdc:0-0
Timestamp: 2025-11-25T14:40:15.063Z
Learning: Applies to **/*.{tsx,ts} : For API GET requests to server, use the `swr` package with hooks like `useSWR` to fetch data

Applied to files:

  • apps/web/providers/SWRProvider.tsx
📚 Learning: 2025-11-25T14:36:18.416Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: apps/web/CLAUDE.md:0-0
Timestamp: 2025-11-25T14:36:18.416Z
Learning: Applies to apps/web/hooks/use*.ts : Use SWR for client-side data fetching with typed responses from GET API routes

Applied to files:

  • apps/web/providers/SWRProvider.tsx
📚 Learning: 2025-11-25T14:36:40.146Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/data-fetching.mdc:0-0
Timestamp: 2025-11-25T14:36:40.146Z
Learning: Applies to **/*{.server,.action}.{ts,tsx} : If in a server file, you can fetch data inline instead of using SWR

Applied to files:

  • apps/web/providers/SWRProvider.tsx
📚 Learning: 2025-11-25T14:37:09.306Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/fullstack-workflow.mdc:0-0
Timestamp: 2025-11-25T14:37:09.306Z
Learning: Applies to apps/web/hooks/use*.ts : Use SWR hooks for client-side data fetching, with hooks stored in `apps/web/hooks/use*.ts` that return typed responses from GET API routes

Applied to files:

  • apps/web/providers/SWRProvider.tsx
📚 Learning: 2025-11-25T14:38:56.992Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/project-structure.mdc:0-0
Timestamp: 2025-11-25T14:38:56.992Z
Learning: Use `swr` for data fetching in deeply nested components

Applied to files:

  • apps/web/providers/SWRProvider.tsx
📚 Learning: 2025-11-25T14:37:35.343Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/hooks.mdc:0-0
Timestamp: 2025-11-25T14:37:35.343Z
Learning: Applies to apps/web/hooks/use*.ts : For data fetching in custom hooks, prefer using `useSWR` and wrap it to handle API endpoint URL, returning data, loading state, error state, and potentially the `mutate` function

Applied to files:

  • apps/web/providers/SWRProvider.tsx
📚 Learning: 2025-11-25T14:38:07.606Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/llm.mdc:0-0
Timestamp: 2025-11-25T14:38:07.606Z
Learning: Applies to apps/web/utils/ai/**/*.ts : Use XML-like tags to structure data in prompts, remove excessive whitespace and truncate long inputs, and format data consistently across similar LLM functions

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:38:07.606Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/llm.mdc:0-0
Timestamp: 2025-11-25T14:38:07.606Z
Learning: Applies to apps/web/utils/ai/**/*.ts : LLM feature functions must follow a standard structure: accept options with `inputData` and `emailAccount` parameters, implement input validation with early returns, define separate system and user prompts, create a Zod schema for response validation, and use `createGenerateObject` to execute the LLM call

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:39:23.326Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/security.mdc:0-0
Timestamp: 2025-11-25T14:39:23.326Z
Learning: Applies to app/api/**/*.ts : Use `SafeError` for error responses to prevent information disclosure - provide generic messages (e.g., 'Rule not found' not 'Rule {id} does not exist for user {userId}') without revealing internal IDs or ownership details

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:51.389Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-11-25T14:36:51.389Z
Learning: Applies to **/*Form.{ts,tsx} : Show validation errors inline next to form fields in the UI

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:53.147Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-11-25T14:36:53.147Z
Learning: Applies to **/*Form.{ts,tsx} : Show validation errors inline next to form fields

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:53.147Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-11-25T14:36:53.147Z
Learning: Applies to **/*.validation.{ts,tsx} : Use descriptive error messages in validation schemas

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:18.416Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: apps/web/CLAUDE.md:0-0
Timestamp: 2025-11-25T14:36:18.416Z
Learning: Applies to apps/web/components/**/*.tsx : Use `result?.serverError` with `toastError` and `toastSuccess` for error handling in form submissions

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:42:08.869Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2025-11-25T14:42:08.869Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Make sure to pass a message value when creating a built-in error

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:51.389Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-11-25T14:36:51.389Z
Learning: Applies to **/*.validation.ts : Use descriptive error messages in Zod validation schemas

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-07-17T04:19:57.099Z
Learnt from: edulelis
Repo: elie222/inbox-zero PR: 576
File: packages/resend/emails/digest.tsx:78-83
Timestamp: 2025-07-17T04:19:57.099Z
Learning: In packages/resend/emails/digest.tsx, the DigestEmailProps type uses `[key: string]: DigestItem[] | undefined | string | Date | undefined` instead of intersection types like `& Record<string, DigestItem[] | undefined>` due to implementation constraints. This was the initial implementation approach and cannot be changed to more restrictive typing.

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:37:09.306Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/fullstack-workflow.mdc:0-0
Timestamp: 2025-11-25T14:37:09.306Z
Learning: Applies to apps/web/components/**/*Form*.tsx : Handle form submission results using `result?.serverError` to show error toasts and `toastSuccess` to show success messages after server action completion

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:39:27.909Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/security.mdc:0-0
Timestamp: 2025-11-25T14:39:27.909Z
Learning: Applies to **/*.ts : Use SafeError for error responses to prevent information disclosure. Generic error messages should not reveal internal IDs, logic, or resource ownership details

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:51.389Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-11-25T14:36:51.389Z
Learning: Applies to **/*Form.{ts,tsx} : Validate form inputs before submission using React Hook Form and Zod resolver

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:37:09.306Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/fullstack-workflow.mdc:0-0
Timestamp: 2025-11-25T14:37:09.306Z
Learning: Applies to apps/web/components/**/*Form*.tsx : Use React Hook Form with Zod validation (`zodResolver`) for form handling, with form components using `register`, `handleSubmit`, and error handling from the hook

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:51.389Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-11-25T14:36:51.389Z
Learning: Applies to **/*Form.{ts,tsx} : Use React Hook Form with Zod for form validation

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:18.416Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: apps/web/CLAUDE.md:0-0
Timestamp: 2025-11-25T14:36:18.416Z
Learning: Applies to apps/web/components/**/*.tsx : Use React Hook Form with Zod validation for form components

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:53.147Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-11-25T14:36:53.147Z
Learning: Applies to **/*Form.{ts,tsx} : Use React Hook Form with Zod for validation in form components

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:40:13.649Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ui-components.mdc:0-0
Timestamp: 2025-11-25T14:40:13.649Z
Learning: Applies to **/*.{tsx,ts,jsx,js} : For text areas in forms, use the `Input` component with `type='text'`, `autosizeTextarea` prop, `rows`, `name`, `placeholder`, `registerProps` from react-hook-form, and `error` props

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:40:13.649Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ui-components.mdc:0-0
Timestamp: 2025-11-25T14:40:13.649Z
Learning: Applies to **/*.{tsx,ts,jsx,js} : For text inputs in forms, use the `Input` component with `type='email'`, `name`, `label`, `registerProps` from react-hook-form, and `error` props

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:53.147Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-11-25T14:36:53.147Z
Learning: Applies to **/*Form.{ts,tsx} : Validate form inputs before submission

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:38:56.992Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/project-structure.mdc:0-0
Timestamp: 2025-11-25T14:38:56.992Z
Learning: Applies to apps/web/components/ui/**/*.tsx : Shadcn UI components are located in `components/ui` directory

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:18.416Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: apps/web/CLAUDE.md:0-0
Timestamp: 2025-11-25T14:36:18.416Z
Learning: Applies to apps/web/**/*.tsx : Use shadcn/ui components when available

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:42:11.919Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/utilities.mdc:0-0
Timestamp: 2025-11-25T14:42:11.919Z
Learning: Applies to utils/**/*.{js,ts,jsx,tsx} : The `utils` folder contains core app logic such as Next.js Server Actions and Gmail API requests

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:40:00.833Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-11-25T14:40:00.833Z
Learning: Applies to **/*.test.{ts,tsx} : Use test helpers `getEmail`, `getEmailAccount`, and `getRule` from `@/__tests__/helpers` for mocking emails, accounts, and rules

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:40:15.063Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ui-components.mdc:0-0
Timestamp: 2025-11-25T14:40:15.063Z
Learning: Applies to **/*.{tsx,ts} : For text inputs, use the `Input` component with `registerProps` for form integration and error handling

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:40:15.063Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ui-components.mdc:0-0
Timestamp: 2025-11-25T14:40:15.063Z
Learning: Applies to **/*.tsx : For text areas, use the `Input` component with `type='text'`, `autosizeTextarea` prop set to true, and `registerProps` for form integration

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
🧬 Code graph analysis (1)
apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx (7)
apps/web/utils/action-display.tsx (1)
  • getActionIcon (86-115)
apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx (1)
  • RuleSectionCard (5-58)
apps/web/utils/reply-tracker/conversation-status-config.ts (1)
  • isConversationStatusType (16-22)
apps/web/utils/rule/consts.ts (1)
  • getRuleConfig (112-116)
apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx (1)
  • ConditionSteps (121-475)
apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx (1)
  • ActionSteps (46-107)
apps/web/components/ui/form.tsx (1)
  • Form (172-172)
⏰ 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). (2)
  • GitHub Check: Socket Security: Pull Request Alerts
  • GitHub Check: test
🔇 Additional comments (3)
apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx (2)

5-6: Form + React Hook Form + Zod integration looks solid

useForm with zodResolver(createRuleBody) plus the shadcn <Form> wrapper and handleSubmit(onSubmit) wiring is clean and matches the React Hook Form + Zod guidelines for *Form components. No changes needed here.

Also applies to: 47-48, 96-120, 122-130, 348-349


140-145: Action field array + icon-backed action type options are well-structured

The new useFieldArray for actions feeding into ActionSteps, and the enriched typeOptions including icon: React.ElementType populated via getActionIcon(ActionType.…), give a clear, extensible shape for action configuration and UI. This should make future action types and icon changes straightforward without touching the form logic.

Also applies to: 273-336, 449-465

apps/web/providers/SWRProvider.tsx (1)

97-106: SWRContext export is clean and correctly typed

Exporting SWRContext with the existing Context interface and defaultContextValue keeps typing tight and enables reuse in consumers/tests without changing runtime behavior. No changes needed here.

Comment on lines +33 to +38
// In development, ignore 404 errors (likely transient HMR errors)
function shouldIgnoreError(error: LoadingContentProps["error"]): boolean {
if (process.env.NODE_ENV !== "development") return false;
const status = (error as { status?: number })?.status;
return status === 404;
}
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, read the LoadingContent.tsx file to understand full context
cat -n apps/web/components/LoadingContent.tsx

Repository: elie222/inbox-zero

Length of output: 1480


🏁 Script executed:

#!/bin/bash
# Search for LoadingContent usage to understand what errors are passed to it
rg -n 'LoadingContent' --type=tsx -g '!LoadingContent.tsx' -A 5 | head -100

Repository: elie222/inbox-zero

Length of output: 89


🏁 Script executed:

#!/bin/bash
# Check error handling patterns - look for how errors are passed to LoadingContent
rg -n 'error=' --type=tsx -A 2 -B 2 | grep -A 2 -B 2 LoadingContent | head -50

Repository: elie222/inbox-zero

Length of output: 89


🏁 Script executed:

#!/bin/bash
# Search for LoadingContent usage patterns to understand error handling
rg 'LoadingContent' -A 3 -B 1 apps/web/components apps/web/app | head -150

Repository: elie222/inbox-zero

Length of output: 10625


🏁 Script executed:

#!/bin/bash
# Search for how errors with status codes are created/passed
rg 'status.*404|404.*status' -B 3 -A 3 apps/web

Repository: elie222/inbox-zero

Length of output: 10644


🏁 Script executed:

#!/bin/bash
# Look for SWR or API error handling that might create 404 errors
rg 'useSWR|fetch|\.catch' -A 2 apps/web/components/LoadingContent.tsx apps/web/app

Repository: elie222/inbox-zero

Length of output: 50374


🏁 Script executed:

#!/bin/bash
# Read SWRProvider to understand 404 handling
head -100 apps/web/providers/SWRProvider.tsx

Repository: elie222/inbox-zero

Length of output: 2509


🏁 Script executed:

#!/bin/bash
# Search for legitimate 404 responses from API routes
rg 'status.*404|status: 404' apps/web/app/api -B 3 -A 1 | head -80

Repository: elie222/inbox-zero

Length of output: 3449


🏁 Script executed:

#!/bin/bash
# Check if there are actual component implementations that rely on 404 errors
rg 'error.*404|404.*error' --type-add 'tsx:!{node_modules,dist,build}' apps/web/components -A 2 -B 2

Repository: elie222/inbox-zero

Length of output: 461


Consider restricting 404 error handling to avoid masking legitimate "resource not found" responses.

The current implementation ignores all 404 errors in development mode, but the codebase legitimately returns 404 for actual resource-not-found scenarios (e.g., missing schedule, email account, user). While SWRProvider retries 404s quickly for transient HMR errors, LoadingContent will still suppress the error after retries exhaust, causing the component to display the loading state indefinitely whenever a real API endpoint returns 404.

To distinguish HMR-related 404s from actual API errors, consider:

  • Checking the request path or error source before ignoring
  • Adding a custom error code or header flag to HMR-specific errors
  • Limiting the ignore logic to specific routes known to have HMR issues
🤖 Prompt for AI Agents
In apps/web/components/LoadingContent.tsx around lines 33-38, the helper
currently silences all 404 errors in development which can hide legitimate API
"not found" responses; change the logic to only ignore 404s that originate from
known HMR/static asset paths or an explicit HMR marker. Specifically, detect the
failing request URL (e.g., error.request?.url or error.config?.url) or a custom
error property and return true only when NODE_ENV is development, status ===
404, and the URL matches HMR/static patterns (like /_next/webpack-hmr,
/_next/static/, or other known dev-only routes) or when a dedicated isHMR
flag/header is present; otherwise return false so real API 404s (e.g., routes
under /api/, /schedules, /emails, /users) are not suppressed.

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: 3

♻️ Duplicate comments (1)
apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx (1)

71-79: Add explicit type="button" to the dropdown trigger.

Same issue as previously flagged - this button lacks type="button" and could trigger form submission.

         <Button
           size="icon"
           variant="ghost"
           className="size-8 mt-1"
           aria-label="More options"
+          type="button"
         >
🧹 Nitpick comments (5)
apps/web/providers/SWRProvider.tsx (1)

171-195: Add errorRetryCount limit to prevent unbounded exponential backoff in development.

SWR defaults to unlimited retries (not limited as previously assumed), so the exponential backoff here grows without bounds. At retryCount=10, delays reach ~85 minutes. Consider adding errorRetryCount: 3 or similar to the dev config to cap retries, or document if unlimited retries are intentional. Since this is dev-only, the practical impact is limited, but it could cause frustrating long waits during development if server errors persist.

apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx (1)

743-773: Consider documenting the -1 sentinel value.

The convertToMinutes function returns -1 for invalid inputs. While this is handled consistently, adding a brief comment or using a named constant would improve clarity for future maintainers.

+// Sentinel value indicating an invalid delay (will be caught by validation)
+const INVALID_DELAY = -1;
+
 // user-friendly UI format to minutes
 function convertToMinutes(value: string, unit: string) {
   const numValue = Number.parseInt(value, 10);
-  if (Number.isNaN(numValue) || numValue <= 0) return -1;
+  if (Number.isNaN(numValue) || numValue <= 0) return INVALID_DELAY;
apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx (3)

350-363: Consider filtering errors with valid messages.

The error display may render items with undefined keys if nested error objects don't have a direct message property. This relates to a past review comment. Consider filtering to only display errors with valid messages.

               <ul className="list-disc">
-                  {Object.values(errors).map((error) => (
-                    <li key={error.message}>{error.message}</li>
-                  ))}
+                  {Object.values(errors)
+                    .filter((error): error is { message: string } => 
+                      typeof error?.message === 'string'
+                    )
+                    .map((error, i) => (
+                      <li key={`${i}-${error.message}`}>{error.message}</li>
+                    ))}
               </ul>

555-556: Add error parameter to catch block.

Per coding guidelines, when creating built-in errors, a message value should be passed. The catch block here has no error parameter, but it does show a toast - consider logging the error for debugging purposes.

-                        } catch {
+                        } catch (error) {
                           toastError({ description: "Failed to delete rule." });
+                          console.error("Delete rule error:", error);
                         } finally {

608-613: Consider extracting allowMultipleConditions to a shared utility.

This function is duplicated in ConditionSteps.tsx. While small, extracting it to a shared location (e.g., @/utils/rule/consts.ts where getRuleConfig already exists) would improve maintainability.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 500fcac and e30a17f.

📒 Files selected for processing (6)
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx (1 hunks)
  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx (1 hunks)
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx (7 hunks)
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx (1 hunks)
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx (1 hunks)
  • apps/web/providers/SWRProvider.tsx (5 hunks)
🧰 Additional context used
📓 Path-based instructions (14)
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/web/CLAUDE.md)

apps/web/**/*.{ts,tsx}: Use TypeScript with strict null checks
Use @/ path aliases for imports from project root
Use proper error handling with try/catch blocks
Format code with Prettier
Follow consistent naming conventions using PascalCase for components
Centralize shared types in dedicated type files

Import specific lodash functions rather than entire lodash library to minimize bundle size (e.g., import groupBy from 'lodash/groupBy')

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/providers/SWRProvider.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
apps/web/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/web/CLAUDE.md)

Follow NextJS app router structure with (app) directory

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
apps/web/**/*.tsx

📄 CodeRabbit inference engine (apps/web/CLAUDE.md)

apps/web/**/*.tsx: Follow tailwindcss patterns with prettier-plugin-tailwindcss for class sorting
Prefer functional components with hooks over class components
Use shadcn/ui components when available
Ensure responsive design with mobile-first approach
Use LoadingContent component for async data with loading and error states

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/providers/SWRProvider.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/data-fetching.mdc)

**/*.{ts,tsx}: For API GET requests to server, use the swr package
Use result?.serverError with toastError from @/components/Toast for error handling in async operations

**/*.{ts,tsx}: Use wrapper functions for Gmail message operations (get, list, batch, etc.) from @/utils/gmail/message.ts instead of direct API calls
Use wrapper functions for Gmail thread operations from @/utils/gmail/thread.ts instead of direct API calls
Use wrapper functions for Gmail label operations from @/utils/gmail/label.ts instead of direct API calls

**/*.{ts,tsx}: For early access feature flags, create hooks using the naming convention use[FeatureName]Enabled that return a boolean from useFeatureFlagEnabled("flag-key")
For A/B test variant flags, create hooks using the naming convention use[FeatureName]Variant that define variant types, use useFeatureFlagVariantKey() with type casting, and provide a default "control" fallback
Use kebab-case for PostHog feature flag keys (e.g., inbox-cleaner, pricing-options-2)
Always define types for A/B test variant flags (e.g., type PricingVariant = "control" | "variant-a" | "variant-b") and provide type safety through type casting

**/*.{ts,tsx}: Don't use primitive type aliases or misleading types
Don't use empty type parameters in type aliases and interfaces
Don't use this and super in static contexts
Don't use any or unknown as type constraints
Don't use the TypeScript directive @ts-ignore
Don't use TypeScript enums
Don't export imported variables
Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions
Don't use TypeScript namespaces
Don't use non-null assertions with the ! postfix operator
Don't use parameter properties in class constructors
Don't use user-defined types
Use as const instead of literal types and type annotations
Use either T[] or Array<T> consistently
Initialize each enum member value explicitly
Use export type for types
Use `impo...

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/providers/SWRProvider.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
apps/web/app/(app)/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/page-structure.mdc)

apps/web/app/(app)/**/*.{ts,tsx}: Components for the page are either put in page.tsx, or in the apps/web/app/(app)/PAGE_NAME folder
If we're in a deeply nested component we will use swr to fetch via API
If you need to use onClick in a component, that component is a client component and file must start with use client

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/prisma-enum-imports.mdc)

Always import Prisma enums from @/generated/prisma/enums instead of @/generated/prisma/client to avoid Next.js bundling errors in client components

Import Prisma using the project's centralized utility: import prisma from '@/utils/prisma'

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/providers/SWRProvider.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
**/*.{tsx,ts}

📄 CodeRabbit inference engine (.cursor/rules/ui-components.mdc)

**/*.{tsx,ts}: Use Shadcn UI and Tailwind for components and styling
Use next/image package for images
For API GET requests to server, use the swr package with hooks like useSWR to fetch data
For text inputs, use the Input component with registerProps for form integration and error handling

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/providers/SWRProvider.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
**/*.{tsx,ts,css}

📄 CodeRabbit inference engine (.cursor/rules/ui-components.mdc)

Implement responsive design with Tailwind CSS using a mobile-first approach

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/providers/SWRProvider.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/ui-components.mdc)

**/*.tsx: Use the LoadingContent component to handle loading states instead of manual loading state management
For text areas, use the Input component with type='text', autosizeTextarea prop set to true, and registerProps for form integration

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/providers/SWRProvider.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{js,jsx,ts,tsx}: Don't use accessKey attribute on any HTML element
Don't set aria-hidden="true" on focusable elements
Don't add ARIA roles, states, and properties to elements that don't support them
Don't use distracting elements like <marquee> or <blink>
Only use the scope prop on <th> elements
Don't assign non-interactive ARIA roles to interactive HTML elements
Make sure label elements have text content and are associated with an input
Don't assign interactive ARIA roles to non-interactive HTML elements
Don't assign tabIndex to non-interactive HTML elements
Don't use positive integers for tabIndex property
Don't include "image", "picture", or "photo" in img alt prop
Don't use explicit role property that's the same as the implicit/default role
Make static elements with click handlers use a valid role attribute
Always include a title element for SVG elements
Give all elements requiring alt text meaningful information for screen readers
Make sure anchors have content that's accessible to screen readers
Assign tabIndex to non-interactive HTML elements with aria-activedescendant
Include all required ARIA attributes for elements with ARIA roles
Make sure ARIA properties are valid for the element's supported roles
Always include a type attribute for button elements
Make elements with interactive roles and handlers focusable
Give heading elements content that's accessible to screen readers (not hidden with aria-hidden)
Always include a lang attribute on the html element
Always include a title attribute for iframe elements
Accompany onClick with at least one of: onKeyUp, onKeyDown, or onKeyPress
Accompany onMouseOver/onMouseOut with onFocus/onBlur
Include caption tracks for audio and video elements
Use semantic elements instead of role attributes in JSX
Make sure all anchors are valid and navigable
Ensure all ARIA properties (aria-*) are valid
Use valid, non-abstract ARIA roles for elements with ARIA roles
Use valid AR...

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/providers/SWRProvider.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{jsx,tsx}: Don't use unnecessary fragments
Don't pass children as props
Don't use the return value of React.render
Make sure all dependencies are correctly specified in React hooks
Make sure all React hooks are called from the top level of component functions
Don't forget key props in iterators and collection literals
Don't define React components inside other components
Don't use event handlers on non-interactive elements
Don't assign to React component props
Don't use both children and dangerouslySetInnerHTML props on the same element
Don't use dangerous JSX props
Don't use Array index in keys
Don't insert comments as text nodes
Don't assign JSX properties multiple times
Don't add extra closing tags for components without children
Use <>...</> instead of <Fragment>...</Fragment>
Watch out for possible "wrong" semicolons inside JSX elements
Make sure void (self-closing) elements don't have children
Don't use target="_blank" without rel="noopener"
Don't use <img> elements in Next.js projects
Don't use <head> elements in Next.js projects

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/providers/SWRProvider.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
!(pages/_document).{jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

Don't use the next/head module in pages/_document.js on Next.js projects

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/providers/SWRProvider.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/utilities.mdc)

**/*.{js,ts,jsx,tsx}: Use lodash utilities for common operations (arrays, objects, strings)
Import specific lodash functions to minimize bundle size (e.g., import groupBy from 'lodash/groupBy')

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/providers/SWRProvider.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
**/*Form.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/form-handling.mdc)

**/*Form.{ts,tsx}: Use React Hook Form with Zod for validation in form components
Validate form inputs before submission
Show validation errors inline next to form fields

Files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
🧠 Learnings (53)
📚 Learning: 2025-11-25T14:38:07.606Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/llm.mdc:0-0
Timestamp: 2025-11-25T14:38:07.606Z
Learning: Applies to apps/web/utils/ai/**/*.ts : Use XML-like tags to structure data in prompts, remove excessive whitespace and truncate long inputs, and format data consistently across similar LLM functions

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:38:07.606Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/llm.mdc:0-0
Timestamp: 2025-11-25T14:38:07.606Z
Learning: Applies to apps/web/utils/ai/**/*.ts : LLM feature functions must follow a standard structure: accept options with `inputData` and `emailAccount` parameters, implement input validation with early returns, define separate system and user prompts, create a Zod schema for response validation, and use `createGenerateObject` to execute the LLM call

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:38:07.606Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/llm.mdc:0-0
Timestamp: 2025-11-25T14:38:07.606Z
Learning: Applies to apps/web/utils/ai/**/*.ts : System prompts must define the LLM's role and task specifications

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
📚 Learning: 2025-11-25T14:42:08.869Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2025-11-25T14:42:08.869Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Always include a `type` attribute for button elements

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
📚 Learning: 2025-11-25T14:42:08.869Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2025-11-25T14:42:08.869Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Make static elements with click handlers use a valid role attribute

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
📚 Learning: 2025-11-25T14:42:08.869Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2025-11-25T14:42:08.869Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Use valid, non-abstract ARIA roles for elements with ARIA roles

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
📚 Learning: 2024-08-23T11:37:26.779Z
Learnt from: aryanprince
Repo: elie222/inbox-zero PR: 210
File: apps/web/app/(app)/stats/NewsletterModal.tsx:2-4
Timestamp: 2024-08-23T11:37:26.779Z
Learning: `MoreDropdown` is a React component and `useUnsubscribeButton` is a custom React hook, and they should not be imported using `import type`.

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
📚 Learning: 2025-11-25T14:42:08.869Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2025-11-25T14:42:08.869Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Make sure label elements have text content and are associated with an input

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
📚 Learning: 2025-11-25T14:42:08.869Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2025-11-25T14:42:08.869Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Use valid ARIA state and property values

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
📚 Learning: 2025-11-25T14:42:08.869Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2025-11-25T14:42:08.869Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Accompany `onClick` with at least one of: `onKeyUp`, `onKeyDown`, or `onKeyPress`

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
📚 Learning: 2025-11-25T14:42:08.869Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2025-11-25T14:42:08.869Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Make sure ARIA properties are valid for the element's supported roles

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
📚 Learning: 2025-11-25T14:42:08.869Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2025-11-25T14:42:08.869Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Don't assign non-interactive ARIA roles to interactive HTML elements

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
📚 Learning: 2025-11-25T14:42:08.869Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2025-11-25T14:42:08.869Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Don't set `aria-hidden="true"` on focusable elements

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
📚 Learning: 2025-11-25T14:38:56.992Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/project-structure.mdc:0-0
Timestamp: 2025-11-25T14:38:56.992Z
Learning: Applies to apps/web/components/ui/**/*.tsx : Shadcn UI components are located in `components/ui` directory

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:40:13.649Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ui-components.mdc:0-0
Timestamp: 2025-11-25T14:40:13.649Z
Learning: Applies to **/*.{tsx,ts,jsx,js} : Use Shadcn UI and Tailwind for components and styling

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
📚 Learning: 2025-11-25T14:36:18.416Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: apps/web/CLAUDE.md:0-0
Timestamp: 2025-11-25T14:36:18.416Z
Learning: Applies to apps/web/**/*.tsx : Use shadcn/ui components when available

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:40:15.063Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ui-components.mdc:0-0
Timestamp: 2025-11-25T14:40:15.063Z
Learning: Applies to **/*.{tsx,ts} : Use Shadcn UI and Tailwind for components and styling

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx
📚 Learning: 2025-11-25T14:38:07.606Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/llm.mdc:0-0
Timestamp: 2025-11-25T14:38:07.606Z
Learning: Applies to apps/web/utils/ai/**/*.ts : LLM feature functions must import from `zod` for schema validation, use `createScopedLogger` from `@/utils/logger`, `chatCompletionObject` and `createGenerateObject` from `@/utils/llms`, and import `EmailAccountWithAI` type from `@/utils/llms/types`

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
📚 Learning: 2025-11-25T14:42:08.869Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2025-11-25T14:42:08.869Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Don't use duplicate conditions in if-else-if chains

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
📚 Learning: 2025-11-25T14:40:13.649Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ui-components.mdc:0-0
Timestamp: 2025-11-25T14:40:13.649Z
Learning: Applies to **/*.{tsx,ts,jsx,js} : For text inputs in forms, use the `Input` component with `type='email'`, `name`, `label`, `registerProps` from react-hook-form, and `error` props

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
📚 Learning: 2025-11-25T14:38:18.874Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/page-structure.mdc:0-0
Timestamp: 2025-11-25T14:38:18.874Z
Learning: Applies to apps/web/app/(app)/**/*.tsx : If nested deeply in components, use `swr` to fetch via API instead of loading data directly

Applied to files:

  • apps/web/providers/SWRProvider.tsx
📚 Learning: 2025-11-25T14:38:23.265Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/page-structure.mdc:0-0
Timestamp: 2025-11-25T14:38:23.265Z
Learning: Applies to apps/web/app/(app)/**/*.{ts,tsx} : If we're in a deeply nested component we will use `swr` to fetch via API

Applied to files:

  • apps/web/providers/SWRProvider.tsx
📚 Learning: 2025-11-25T14:36:18.416Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: apps/web/CLAUDE.md:0-0
Timestamp: 2025-11-25T14:36:18.416Z
Learning: Applies to apps/web/hooks/use*.ts : Use SWR for client-side data fetching with typed responses from GET API routes

Applied to files:

  • apps/web/providers/SWRProvider.tsx
📚 Learning: 2025-11-25T14:40:13.649Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ui-components.mdc:0-0
Timestamp: 2025-11-25T14:40:13.649Z
Learning: Applies to **/*.{tsx,ts,jsx,js} : For API get requests to server, use the `swr` package with `useSWR` hook

Applied to files:

  • apps/web/providers/SWRProvider.tsx
📚 Learning: 2025-11-25T14:40:15.063Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ui-components.mdc:0-0
Timestamp: 2025-11-25T14:40:15.063Z
Learning: Applies to **/*.{tsx,ts} : For API GET requests to server, use the `swr` package with hooks like `useSWR` to fetch data

Applied to files:

  • apps/web/providers/SWRProvider.tsx
📚 Learning: 2025-11-25T14:36:36.276Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/data-fetching.mdc:0-0
Timestamp: 2025-11-25T14:36:36.276Z
Learning: Applies to **/*.{ts,tsx} : For API GET requests to server, use the `swr` package

Applied to files:

  • apps/web/providers/SWRProvider.tsx
📚 Learning: 2025-11-25T14:37:09.306Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/fullstack-workflow.mdc:0-0
Timestamp: 2025-11-25T14:37:09.306Z
Learning: Applies to apps/web/hooks/use*.ts : Use SWR hooks for client-side data fetching, with hooks stored in `apps/web/hooks/use*.ts` that return typed responses from GET API routes

Applied to files:

  • apps/web/providers/SWRProvider.tsx
📚 Learning: 2025-11-25T14:37:30.660Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/hooks.mdc:0-0
Timestamp: 2025-11-25T14:37:30.660Z
Learning: Applies to apps/web/hooks/use*.ts : For data fetching, prefer using `useSWR` and follow the data-fetching guidelines

Applied to files:

  • apps/web/providers/SWRProvider.tsx
📚 Learning: 2025-11-25T14:36:36.276Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/data-fetching.mdc:0-0
Timestamp: 2025-11-25T14:36:36.276Z
Learning: For mutating data, use Next.js server actions instead of SWR

Applied to files:

  • apps/web/providers/SWRProvider.tsx
📚 Learning: 2025-11-25T14:38:56.992Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/project-structure.mdc:0-0
Timestamp: 2025-11-25T14:38:56.992Z
Learning: Use `swr` for data fetching in deeply nested components

Applied to files:

  • apps/web/providers/SWRProvider.tsx
📚 Learning: 2025-11-25T14:36:40.146Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/data-fetching.mdc:0-0
Timestamp: 2025-11-25T14:36:40.146Z
Learning: Applies to **/*{.server,.action}.{ts,tsx} : If in a server file, you can fetch data inline instead of using SWR

Applied to files:

  • apps/web/providers/SWRProvider.tsx
📚 Learning: 2025-11-25T14:37:35.343Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/hooks.mdc:0-0
Timestamp: 2025-11-25T14:37:35.343Z
Learning: Applies to apps/web/hooks/use*.ts : For data fetching in custom hooks, prefer using `useSWR` and wrap it to handle API endpoint URL, returning data, loading state, error state, and potentially the `mutate` function

Applied to files:

  • apps/web/providers/SWRProvider.tsx
📚 Learning: 2025-11-25T14:39:23.326Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/security.mdc:0-0
Timestamp: 2025-11-25T14:39:23.326Z
Learning: Applies to app/api/**/*.ts : Use `SafeError` for error responses to prevent information disclosure - provide generic messages (e.g., 'Rule not found' not 'Rule {id} does not exist for user {userId}') without revealing internal IDs or ownership details

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:51.389Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-11-25T14:36:51.389Z
Learning: Applies to **/*Form.{ts,tsx} : Show validation errors inline next to form fields in the UI

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:53.147Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-11-25T14:36:53.147Z
Learning: Applies to **/*Form.{ts,tsx} : Show validation errors inline next to form fields

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:53.147Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-11-25T14:36:53.147Z
Learning: Applies to **/*.validation.{ts,tsx} : Use descriptive error messages in validation schemas

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:18.416Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: apps/web/CLAUDE.md:0-0
Timestamp: 2025-11-25T14:36:18.416Z
Learning: Applies to apps/web/components/**/*.tsx : Use `result?.serverError` with `toastError` and `toastSuccess` for error handling in form submissions

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:42:08.869Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2025-11-25T14:42:08.869Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Make sure to pass a message value when creating a built-in error

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:51.389Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-11-25T14:36:51.389Z
Learning: Applies to **/*.validation.ts : Use descriptive error messages in Zod validation schemas

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-07-17T04:19:57.099Z
Learnt from: edulelis
Repo: elie222/inbox-zero PR: 576
File: packages/resend/emails/digest.tsx:78-83
Timestamp: 2025-07-17T04:19:57.099Z
Learning: In packages/resend/emails/digest.tsx, the DigestEmailProps type uses `[key: string]: DigestItem[] | undefined | string | Date | undefined` instead of intersection types like `& Record<string, DigestItem[] | undefined>` due to implementation constraints. This was the initial implementation approach and cannot be changed to more restrictive typing.

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:37:09.306Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/fullstack-workflow.mdc:0-0
Timestamp: 2025-11-25T14:37:09.306Z
Learning: Applies to apps/web/components/**/*Form*.tsx : Handle form submission results using `result?.serverError` to show error toasts and `toastSuccess` to show success messages after server action completion

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:39:27.909Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/security.mdc:0-0
Timestamp: 2025-11-25T14:39:27.909Z
Learning: Applies to **/*.ts : Use SafeError for error responses to prevent information disclosure. Generic error messages should not reveal internal IDs, logic, or resource ownership details

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:51.389Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-11-25T14:36:51.389Z
Learning: Applies to **/*Form.{ts,tsx} : Validate form inputs before submission using React Hook Form and Zod resolver

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:37:09.306Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/fullstack-workflow.mdc:0-0
Timestamp: 2025-11-25T14:37:09.306Z
Learning: Applies to apps/web/components/**/*Form*.tsx : Use React Hook Form with Zod validation (`zodResolver`) for form handling, with form components using `register`, `handleSubmit`, and error handling from the hook

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:51.389Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-11-25T14:36:51.389Z
Learning: Applies to **/*Form.{ts,tsx} : Use React Hook Form with Zod for form validation

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:18.416Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: apps/web/CLAUDE.md:0-0
Timestamp: 2025-11-25T14:36:18.416Z
Learning: Applies to apps/web/components/**/*.tsx : Use React Hook Form with Zod validation for form components

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:36:53.147Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-11-25T14:36:53.147Z
Learning: Applies to **/*Form.{ts,tsx} : Use React Hook Form with Zod for validation in form components

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:40:13.649Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ui-components.mdc:0-0
Timestamp: 2025-11-25T14:40:13.649Z
Learning: Applies to **/*.{tsx,ts,jsx,js} : For text areas in forms, use the `Input` component with `type='text'`, `autosizeTextarea` prop, `rows`, `name`, `placeholder`, `registerProps` from react-hook-form, and `error` props

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
  • apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx
📚 Learning: 2025-11-25T14:36:53.147Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/form-handling.mdc:0-0
Timestamp: 2025-11-25T14:36:53.147Z
Learning: Applies to **/*Form.{ts,tsx} : Validate form inputs before submission

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:42:11.919Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/utilities.mdc:0-0
Timestamp: 2025-11-25T14:42:11.919Z
Learning: Applies to utils/**/*.{js,ts,jsx,tsx} : The `utils` folder contains core app logic such as Next.js Server Actions and Gmail API requests

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:40:00.833Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/testing.mdc:0-0
Timestamp: 2025-11-25T14:40:00.833Z
Learning: Applies to **/*.test.{ts,tsx} : Use test helpers `getEmail`, `getEmailAccount`, and `getRule` from `@/__tests__/helpers` for mocking emails, accounts, and rules

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:40:15.063Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ui-components.mdc:0-0
Timestamp: 2025-11-25T14:40:15.063Z
Learning: Applies to **/*.{tsx,ts} : For text inputs, use the `Input` component with `registerProps` for form integration and error handling

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
📚 Learning: 2025-11-25T14:40:15.063Z
Learnt from: CR
Repo: elie222/inbox-zero PR: 0
File: .cursor/rules/ui-components.mdc:0-0
Timestamp: 2025-11-25T14:40:15.063Z
Learning: Applies to **/*.tsx : For text areas, use the `Input` component with `type='text'`, `autosizeTextarea` prop set to true, and `registerProps` for form integration

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx
🧬 Code graph analysis (2)
apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx (2)
apps/web/components/new-landing/common/Card.tsx (1)
  • Card (67-105)
apps/web/components/Typography.tsx (1)
  • TypographyH3 (125-125)
apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx (5)
apps/web/utils/actions/rule.validation.ts (2)
  • ZodCondition (62-62)
  • CreateRuleBody (194-194)
apps/web/utils/reply-tracker/conversation-status-config.ts (1)
  • isConversationStatusType (16-22)
apps/web/app/(app)/[emailAccountId]/assistant/RuleSteps.tsx (1)
  • RuleSteps (7-40)
apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx (1)
  • RuleStep (185-237)
apps/web/utils/rule/consts.ts (1)
  • getRuleConfig (112-116)
⏰ 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: test
🔇 Additional comments (15)
apps/web/providers/SWRProvider.tsx (4)

38-51: LGTM!

Good defensive handling for non-JSON error responses. The try/catch gracefully handles HMR 404s in development while still logging unexpected failures in production for debugging.


80-88: LGTM!

The error shape with info and status properties aligns well with SWR's documented error handling pattern, providing consistent error structure for downstream consumers.


118-118: LGTM!

Exporting SWRContext enables direct context consumption when needed, which is useful for advanced use cases outside the default provider hooks.


162-162: LGTM!

Clean integration that spreads dev-only config without affecting production behavior.

apps/web/app/(app)/[emailAccountId]/assistant/RuleSectionCard.tsx (1)

1-37: LGTM!

Clean, well-structured component following shadcn/ui patterns. Good use of conditional rendering for optional errors and children blocks, and proper dark mode support via Tailwind classes.

apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx (1)

185-237: LGTM!

The main RuleStep component is well-structured with clean composition of layout primitives and action buttons. Props are properly typed with appropriate optional handlers.

apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx (2)

46-107: LGTM!

Well-structured component with proper typing integration with CreateRuleBody. Clean mapping of action fields with appropriate keys.


617-658: LGTM!

Good helper dialog for showing variable examples. The template examples are clear and instructive for users.

apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx (5)

44-61: LGTM!

Clean conversion logic between backend ZodCondition and UI-level condition types. The comment explaining body mapping to subject in UI is helpful.


165-176: LGTM!

The useEffect correctly initializes the first condition to prompt type only when empty, preserving existing static conditions. Good defensive logic.


256-271: LGTM!

The duplicate detection logic correctly filters out undefined values before creating the Set, addressing the previously identified bug where multiple unselected conditions would incorrectly trigger the duplicate error.


277-300: LGTM!

Good handling of AI prompt positioning - automatically moving it to the first position while preserving existing condition data. The logic correctly handles both configured and unconfigured conditions during reordering.


449-534: LGTM!

Clean per-type field rendering with proper registerProps integration, inline error handling, and helpful tooltips. Good use of TextareaAutosize for the prompt field.

apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx (2)

273-336: LGTM!

Clean integration of action icons via getActionIcon. The memoization dependencies are correct, and all action types are properly mapped.


386-460: LGTM!

Clean integration of modular components. The RuleSectionCard wrapper provides consistent styling and error display, while ConditionSteps and ActionSteps handle their respective domains well. Props are properly threaded through.

Comment on lines +483 to +500
<Button
size="xs"
variant="ghost"
className="flex items-center gap-1 text-xs text-muted-foreground"
onClick={() => setExpandedFields(!expandedFields)}
>
{expandedFields ? (
<>
<ChevronDownIcon className="h-3.5 w-3.5" />
Hide extra fields
</>
) : (
<>
<ChevronRightIcon className="h-3.5 w-3.5" />
Show all fields
</>
)}
</Button>
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 | 🟡 Minor

Add type="button" to the expand/collapse button.

This button is inside a form context and toggles UI state. Without type="button", it defaults to submit behavior. As per coding guidelines, always include a type attribute for button elements.

               <Button
                 size="xs"
                 variant="ghost"
                 className="flex items-center gap-1 text-xs text-muted-foreground"
                 onClick={() => setExpandedFields(!expandedFields)}
+                type="button"
               >
📝 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
<Button
size="xs"
variant="ghost"
className="flex items-center gap-1 text-xs text-muted-foreground"
onClick={() => setExpandedFields(!expandedFields)}
>
{expandedFields ? (
<>
<ChevronDownIcon className="h-3.5 w-3.5" />
Hide extra fields
</>
) : (
<>
<ChevronRightIcon className="h-3.5 w-3.5" />
Show all fields
</>
)}
</Button>
<Button
size="xs"
variant="ghost"
className="flex items-center gap-1 text-xs text-muted-foreground"
onClick={() => setExpandedFields(!expandedFields)}
type="button"
>
{expandedFields ? (
<>
<ChevronDownIcon className="h-3.5 w-3.5" />
Hide extra fields
</>
) : (
<>
<ChevronRightIcon className="h-3.5 w-3.5" />
Show all fields
</>
)}
</Button>
🤖 Prompt for AI Agents
In apps/web/app/(app)/[emailAccountId]/assistant/ActionSteps.tsx around lines
483 to 500, the expand/collapse Button is inside a form and lacks an explicit
type, causing it to default to submit behavior; update the Button props to
include type="button" so it only toggles UI state (setExpandedFields) and does
not submit the form.

Comment on lines +571 to +575
{onCancel && (
<Button variant="outline" size="sm" onClick={onCancel}>
Cancel
</Button>
)}
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 | 🟡 Minor

Add type="button" to the Cancel button.

The Cancel button should not submit the form. Without an explicit type="button", it could inadvertently submit in some browsers.

             {onCancel && (
-              <Button variant="outline" size="sm" onClick={onCancel}>
+              <Button variant="outline" size="sm" onClick={onCancel} type="button">
                 Cancel
               </Button>
             )}
📝 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
{onCancel && (
<Button variant="outline" size="sm" onClick={onCancel}>
Cancel
</Button>
)}
{onCancel && (
<Button variant="outline" size="sm" onClick={onCancel} type="button">
Cancel
</Button>
)}
🤖 Prompt for AI Agents
In apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx around lines 571
to 575, the Cancel button is missing an explicit type and may submit the form in
some browsers; update the Button element to include type="button" so clicking
Cancel does not trigger form submission and keeps its current onClick handler
intact.

Comment on lines +24 to +34
return (
<Button
size="icon"
variant="ghost"
className="size-8 mt-1"
aria-label={ariaLabel}
onClick={onClick}
>
<TrashIcon className="size-4 text-muted-foreground" />
</Button>
);
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 | 🟡 Minor

Add explicit type="button" to prevent form submission.

This DeleteButton is rendered inside RuleForm which wraps content in a <form> element. Without an explicit type="button", clicking could inadvertently submit the form. As per coding guidelines, buttons should always include a type attribute.

     <Button
       size="icon"
       variant="ghost"
       className="size-8 mt-1"
       aria-label={ariaLabel}
       onClick={onClick}
+      type="button"
     >
📝 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
return (
<Button
size="icon"
variant="ghost"
className="size-8 mt-1"
aria-label={ariaLabel}
onClick={onClick}
>
<TrashIcon className="size-4 text-muted-foreground" />
</Button>
);
return (
<Button
size="icon"
variant="ghost"
className="size-8 mt-1"
aria-label={ariaLabel}
onClick={onClick}
type="button"
>
<TrashIcon className="size-4 text-muted-foreground" />
</Button>
);
🤖 Prompt for AI Agents
In apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx around lines 24 to
34, the DeleteButton lacks an explicit type which can cause it to submit the
parent form; add type="button" to the Button element (e.g., pass a type prop) so
the button does not trigger form submission, keeping all other props and
behavior unchanged.

@socket-security
Copy link

socket-security bot commented Dec 16, 2025

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm safer-buffer is 94.0% likely obfuscated

Confidence: 0.94

Location: Package overview

From: pnpm-lock.yamlnpm/safer-buffer@2.1.2

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/safer-buffer@2.1.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

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: 3

♻️ Duplicate comments (4)
apps/web/utils/condition.ts (1)

149-171: flattenConditions will overwrite fields when merging per-field STATIC conditions.

The new getConditions function creates separate STATIC conditions per field (e.g., {from: "test", to: null} and {from: null, to: "user"}). However, flattenConditions unconditionally assigns all fields at lines 158-161, causing later conditions to overwrite earlier values with null.

Example: [{from: "a@test.com", to: null}, {from: null, to: "b@test.com"}] results in {from: null, to: "b@test.com"} — losing the from value.

Apply this diff to merge only non-null values:

       case ConditionType.STATIC:
-        acc.to = condition.to;
-        acc.from = condition.from;
-        acc.subject = condition.subject;
-        acc.body = condition.body;
+        if (condition.to !== null) acc.to = condition.to;
+        if (condition.from !== null) acc.from = condition.from;
+        if (condition.subject !== null) acc.subject = condition.subject;
+        if (condition.body !== null) acc.body = condition.body;
         break;
apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx (2)

24-34: Add explicit type="button" to prevent form submission.

This button is rendered inside forms via RuleForm. Without an explicit type="button", it defaults to type="submit" which could inadvertently submit the form. As per coding guidelines, buttons should always include a type attribute.

     <Button
       size="icon"
       variant="ghost"
       className="size-8 mt-1"
       aria-label={ariaLabel}
       onClick={onClick}
+      type="button"
     >

69-79: Add explicit type="button" to dropdown trigger.

Same issue as DeleteButton — this button could trigger form submission without an explicit type.

         <Button
           size="icon"
           variant="ghost"
           className="size-8 mt-1"
           aria-label="More options"
+          type="button"
         >
apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx (1)

579-583: Add type="button" to the Cancel button.

Without an explicit type="button", the Cancel button may inadvertently submit the form in some browsers. As per coding guidelines, always include a type attribute for button elements.

             {onCancel && (
-              <Button variant="outline" size="sm" onClick={onCancel}>
+              <Button variant="outline" size="sm" onClick={onCancel} type="button">
                 Cancel
               </Button>
             )}
🧹 Nitpick comments (7)
apps/web/app/(app)/[emailAccountId]/setup/StatsCardGrid.tsx (2)

86-86: Consider using a stable identifier for the key prop.

Using array index as the key is acceptable here since the items array is static, but if the items ever become dynamic (reorderable, filterable, etc.), this could cause React rendering issues. Consider using item.title as a stable identifier instead:

-            <div key={index} className="flex-1 p-6">
+            <div key={item.title} className="flex-1 p-6">

73-109: Clean responsive layout with good accessibility.

The rendering logic follows best practices:

  • Mobile-first responsive design with proper breakpoints
  • Dark mode support through variant classes
  • Tooltip accessibility handled by shadcn/ui components
  • Semantic HTML structure

Optional: Consider using the cn() utility (from @/utils/cn or similar) instead of template literals for className construction to benefit from better type safety and class merging:

import { cn } from "@/utils/cn";

// Instead of:
className={`size-10 mb-4 flex items-center justify-center rounded-lg ${variant.iconBg}`}

// Use:
className={cn("size-10 mb-4 flex items-center justify-center rounded-lg", variant.iconBg)}
apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx (2)

411-412: Improve left-alignment with items-start for better wrapping.

When action badges wrap to multiple lines, items-start ensures consistent left alignment across rows.

-    <div className="flex gap-1 sm:gap-2 flex-wrap min-w-0 justify-start">
+    <div className="flex gap-1 sm:gap-2 flex-wrap min-w-0 justify-start items-start">

463-476: Empty tooltip content when no conditions exist.

When a rule has neither AI nor static conditions, the tooltip will show an empty string from conditionsToString(rule), and the icons div will be empty. Consider hiding the component entirely in this case.

 function StaticConditionsIcon({ rule }: { rule: RulesResponse[number] }) {
+  const hasConditions = hasAiConditions(rule) || hasStaticConditions(rule);
+  if (!hasConditions) return null;
+
   return (
     <Tooltip content={conditionsToString(rule)}>
       <div className="flex items-center gap-2">
         {hasAiConditions(rule) && (
           <BotIcon className="size-4 text-muted-foreground" />
         )}
         {hasStaticConditions(rule) && (
           <FilterIcon className="size-4 text-muted-foreground" />
         )}
       </div>
     </Tooltip>
   );
 }
apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx (2)

302-302: Controlled Select with potentially undefined value.

Setting value={uiType || undefined} when uiType is already undefined is redundant. The || undefined doesn't change behavior since undefined || undefined is still undefined. This is minor but could be simplified.

-                    value={uiType || undefined}
+                    value={uiType}

420-426: TextareaAutosize styling uses direct border classes instead of design tokens.

The textarea uses hardcoded border-border and focus:border-black which may not align with dark mode. Consider using Tailwind's ring utilities for consistency with shadcn/ui patterns.

                        <TextareaAutosize
-                          className="block w-full flex-1 whitespace-pre-wrap rounded-md border border-border bg-background shadow-sm focus:border-black focus:ring-black sm:text-sm"
+                          className="block w-full flex-1 whitespace-pre-wrap rounded-md border border-input bg-background px-3 py-2 shadow-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 sm:text-sm"
                           minRows={3}
                           rows={3}
                           {...register(`conditions.${index}.instructions`)}
                           placeholder="e.g. Newsletters, regular content from publications, blogs, or services I've subscribed to"
                         />
apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx (1)

275-279: formErrors may miss nested validation errors.

The filter Boolean(error.message) only catches top-level errors with a direct message property. Nested errors from arrays like conditions or actions have different structures. Consider also checking for root.message on nested errors if you want to surface them here.

   const formErrors = useMemo(() => {
-    return Object.values(formState.errors)
-      .filter((error): error is { message: string } => Boolean(error.message))
-      .map((error) => error.message);
+    const messages: string[] = [];
+    for (const error of Object.values(formState.errors)) {
+      if (error && typeof error === 'object') {
+        if ('message' in error && typeof error.message === 'string') {
+          messages.push(error.message);
+        }
+        if ('root' in error && error.root && typeof error.root === 'object' && 'message' in error.root && typeof error.root.message === 'string') {
+          messages.push(error.root.message);
+        }
+      }
+    }
+    return messages;
   }, [formState]);

Comment on lines +44 to +61
// Convert backend condition to UI type
function getUIConditionType(
condition: ZodCondition,
): UIConditionType | undefined {
if (condition.type === ConditionType.AI) {
return "prompt";
}
// For STATIC conditions, determine which field is populated
// With the new structure, each STATIC condition should only have one field
// We set the active field to "" (empty string) and others to null
// So we check which field is not null to determine the UI type
if (condition.from !== null) return "from";
if (condition.to !== null) return "to";
if (condition.subject !== null) return "subject";
if (condition.body !== null) return "subject"; // body maps to subject in UI
// Return undefined if no field is populated (new/unselected condition)
return undefined;
}
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Check references to body in the ConditionSteps file and condition utilities
rg -n "body" apps/web/app/\(app\)/\[emailAccountId\]/assistant/ConditionSteps.tsx -A 2 -B 2

Repository: elie222/inbox-zero

Length of output: 861


🏁 Script executed:

# Search for UIConditionType definition to understand the type structure
rg -n "type UIConditionType|UIConditionType =" apps/web --type ts --type tsx

Repository: elie222/inbox-zero

Length of output: 89


🏁 Script executed:

# Check condition utilities and types
rg -n "ConditionType|ZodCondition" apps/web/utils/condition.ts -A 3 -B 1 | head -100

Repository: elie222/inbox-zero

Length of output: 2766


🏁 Script executed:

# Look for any form or UI components handling body conditions
rg -n "body.*condition|condition.*body" apps/web/app/\(app\)/\[emailAccountId\]/assistant/ --type tsx -B 2 -A 2

Repository: elie222/inbox-zero

Length of output: 89


🏁 Script executed:

# Find UIConditionType definition
rg "UIConditionType" apps/web/app/\(app\)/\[emailAccountId\]/assistant/ConditionSteps.tsx

Repository: elie222/inbox-zero

Length of output: 1069


🏁 Script executed:

# Check the rule validation schema to understand body field in ZodCondition
rg -n "body" apps/web/utils/actions/rule.validation.ts -A 2 -B 2

Repository: elie222/inbox-zero

Length of output: 518


🏁 Script executed:

# Look for any body-related condition form or display logic
rg -n "body" apps/web/app/\(app\)/\[emailAccountId\]/assistant/ -B 2 -A 2

Repository: elie222/inbox-zero

Length of output: 6993


🏁 Script executed:

# Check if body is ever actually set/populated in conditions
rg -n "condition\.body\s*=" apps/web --type ts

Repository: elie222/inbox-zero

Length of output: 44


The body-to-subject mapping is a bug that prevents body conditions from being properly edited in the UI.

The backend fully supports body as a separate condition field, and ResultDisplay.tsx displays body conditions with their own label (Body: "..."). However, UIConditionType only defines options for "from" | "to" | "subject" | "prompt" with no "body" option. Line 58 maps condition.body to "subject" as a workaround, which means body conditions cannot be properly created or edited through the UI—users selecting a body condition would instead edit it as a subject condition. To fix this, add "body" to the UIConditionType definition and update the condition selection UI to support it.

🤖 Prompt for AI Agents
In apps/web/app/(app)/[emailAccountId]/assistant/ConditionSteps.tsx around lines
44 to 61, the function maps backend STATIC condition.body to "subject" which
prevents body conditions from being edited correctly; add "body" to the
UIConditionType union/type definition, change this function to return "body"
when condition.body is populated (instead of "subject"), and update the
condition selection UI/control (and any related type guards or switch branches)
to include and render the "body" option so body conditions can be created and
edited independently from subject.

Comment on lines +523 to 573
{rule.id && (
<Button
size="sm"
variant="outline"
Icon={TrashIcon}
loading={isDeleting}
disabled={isSubmitting}
onClick={async () => {
const yes = confirm(
"Are you sure you want to delete this rule?",
);
if (yes) {
try {
setIsDeleting(true);
const result = await deleteRuleAction(
emailAccountId,
{
id: rule.id!,
},
);
if (result?.serverError) {
toastError({
description: result.serverError,
});
} else {
toastSuccess({
description: "The rule has been deleted.",
});

if (isDialog && onSuccess) {
onSuccess();
}

router.push(
prefixPath(
emailAccountId,
"/automation?tab=rules",
),
);
}
} catch {
toastError({ description: "Failed to delete rule." });
} finally {
setIsDeleting(false);
}
}
}}
>
Delete rule
</Button>
)}
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 | 🟡 Minor

Delete button inside Dialog should have type="button".

The delete button is inside a <form> and uses onClick with async logic. Without type="button", it could trigger form submission. Though the Button component may handle this, being explicit is safer.

                   <Button
                     size="sm"
                     variant="outline"
                     Icon={TrashIcon}
                     loading={isDeleting}
                     disabled={isSubmitting}
+                    type="button"
                     onClick={async () => {
🤖 Prompt for AI Agents
In apps/web/app/(app)/[emailAccountId]/assistant/RuleForm.tsx around lines 523
to 573, the Delete button rendered inside a form lacks an explicit type and may
trigger form submission when clicked; update the Button element to include
type="button" to prevent accidental form submit, leaving the existing onClick
async handler and disabled/loading props unchanged.

Comment on lines +1 to +16
import { Button } from "@/components/ui/button";
import {
TrashIcon,
MoreHorizontalIcon,
ClockIcon,
SparklesIcon,
PenLineIcon,
} from "lucide-react";
import { cn } from "@/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";

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 | 🟡 Minor

Missing 'use client' directive.

This component uses onClick handlers (lines 30, 82, 88, 94, 100, 106, 112) which require client-side interactivity. Per coding guidelines, components with onClick must be client components.

Add the directive at the top of the file:

+'use client';
+
 import { Button } from "@/components/ui/button";
🤖 Prompt for AI Agents
In apps/web/app/(app)/[emailAccountId]/assistant/RuleStep.tsx around lines 1 to
16, this React component uses multiple onClick handlers (lines ~30, 82, 88, 94,
100, 106, 112) and therefore must be a client component; add the "use client"
directive as the very first line of the file (before any imports) so the file
runs on the client and supports interactivity, then verify linting and that no
server-only APIs are used in the component.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Comments