Skip to content

Refine agent visibility controls and browsing filters#1609

Open
ivaavimusic wants to merge 5 commits intorecoupable:testfrom
ivaavimusic:agents-visibility-filters
Open

Refine agent visibility controls and browsing filters#1609
ivaavimusic wants to merge 5 commits intorecoupable:testfrom
ivaavimusic:agents-visibility-filters

Conversation

@ivaavimusic
Copy link
Copy Markdown
Collaborator

@ivaavimusic ivaavimusic commented Mar 30, 2026

Summary

This PR refines how public and private agent visibility is managed and viewed.

Changes

  • replaces the agents page public/private switch with a smaller segmented filter
  • simplifies create and edit agent visibility to a compact inline row
  • keeps email-sharing inputs directly below when private mode is enabled
  • adds fallback category tags so the modal remains usable when local data is sparse
  • refactors create and edit forms to share a dedicated agent form hook

Why

The previous visibility controls were harder to scan and took up too much space in the modal. This makes visibility feel like a normal form field while improving browsing on the agents page.

Risk

Low to medium. The change is mostly UI and state orchestration around existing visibility behavior.

Summary by CodeRabbit

  • New Features

    • Added help text for email sharing and a redesigned public/private visibility control.
  • Improvements

    • Centralized agent form flow with improved reset and edit behavior for more reliable saves.
    • Tag selector now omits system tags from choices.
    • Sharing enhancements: supports invited-email shares alongside account-based shares and better sync of shared templates.

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 30, 2026

@ivaavimusic is attempting to deploy a commit to the Recoupable Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 30, 2026

📝 Walkthrough

Walkthrough

Refactors agent form ownership into a reusable useAgentForm hook, adds useEditAgentTemplate for edits, extracts visibility UI into AgentVisibilityControl and AgentsVisibilityFilter, and updates Supabase share handling to support account-based and email-invited shares.

Changes

Cohort / File(s) Summary
Form hooks & edit mutation
hooks/useAgentForm.ts, hooks/useEditAgentTemplate.ts
Adds useAgentForm to create/validate the RHF form and sync shareEmails with isPrivate; adds useEditAgentTemplate to perform PATCH /api/agent-templates, merge existing + new shares, and invalidate templates cache.
Create/Edit dialog & form ownership
components/Agents/CreateAgentDialog.tsx, components/Agents/AgentEditDialog.tsx, components/Agents/CreateAgentForm.tsx
Dialogs now instantiate form via useAgentForm() and reset on dialog lifecycle; CreateAgentForm accepts an external form prop; AgentEditDialog wires submission through useEditAgentTemplate and removes inline mutation/query client usage.
Visibility UI components
components/Agents/AgentVisibilityControl.tsx, components/Agents/AgentsVisibilityFilter.tsx, components/Agents/PrivacySection.tsx, components/Agents/Agents.tsx
Introduces AgentVisibilityControl (RHF-bound switch) and AgentsVisibilityFilter (button-based filter). Replaces inline Switch usage with these components and updates PrivacySection to use AgentVisibilityControl.
Minor frontend adjustments
components/Agents/TagSelector.tsx, components/Agents/EmailShareInput.tsx
TagSelector precomputes available tags excluding "Recommended"; EmailShareInput adds help text under the label.
Supabase share handling additions
lib/supabase/agent_templates/splitShareEmailsByAccount.ts, lib/supabase/agent_templates/insertAgentTemplateEmailShares.ts, lib/supabase/agent_templates/deleteAgentTemplateEmailShares.ts, lib/supabase/agent_templates/getAgentTemplateEmailSharesByTemplateIds.ts
Adds utilities to split emails into existing account IDs vs invited emails, insert/delete email-based share rows, and query email-shares by template IDs.
Share update & retrieval logic
lib/supabase/agent_templates/updateAgentTemplateShares.ts, lib/supabase/agent_templates/createAgentTemplateShares.ts, lib/supabase/agent_templates/getSharedEmailsForTemplates.ts, lib/supabase/agent_templates/getSharedTemplatesForUser.ts
Reworks share update/create flows to use split account/email logic, concurrently handle account and email share rows, and merge invited email shares into shared-email lookups and user-visible templates.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant UI as AgentEditDialog / CreateAgentForm
  participant Hook as useEditAgentTemplate
  participant API as /api/agent-templates
  participant DB as Supabase

  User->>UI: Submit form (form values including shareEmails, isPrivate)
  UI->>Hook: mutate(values)
  Hook->>Hook: compute finalShareEmails (merge currentSharedEmails + new when isPrivate)
  Hook->>API: PATCH payload { id, title, description, prompt, tags, isPrivate, shareEmails }
  API->>DB: update agent_templates row
  API->>DB: delete/insert account-based and email-based share rows (split flow)
  DB-->>API: result
  API-->>Hook: success response
  Hook->>UI: onSuccess callback (invalidate cache / close dialog)
  UI->>User: UI updates (dialog closes / list refreshed)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • sweetmantech

Poem

Forms are born where hooks reside,
Visibility split, no state to hide,
Emails split, shares flow both ways,
Dialogs lean on cleaner plays —
Small pieces dance and systems glide. 🎉

🚥 Pre-merge checks | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning Pull request violates DRY (error handling repeated 6+ times), Clean Code (input validation bypassed in splitShareEmailsByAccount), SRP (getSharedTemplatesForUser mixes multiple concerns), and atomicity principles (updateAgentTemplateShares performs non-atomic operations). Extract shared error handler utility, add type guards for input validation, refactor updateAgentTemplateShares for atomic operations, eliminate template processing duplication, and split getSharedTemplatesForUser by responsibility.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a04a94f268

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines 90 to +94
useEffect(() => {
if (open) {
setCurrentSharedEmails(agent.shared_emails || []);
form.reset({
title: agent.title,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Limit edit-form reset to initial dialog open

The new reset effect now depends on the entire agent object, so while the dialog is open any background query refresh that replaces agent (for example React Query refetch on window focus) will call form.reset(...) and discard unsaved edits. This is a user-facing data-loss regression in AgentEditDialog because typing into the form can be silently wiped mid-edit; the reset should be keyed to opening the dialog (or a stable identity change like agent.id) rather than every object refresh.

Useful? React with 👍 / 👎.

Comment on lines +19 to +21
<Switch
id="isPrivate"
checked={isPrivate}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add an accessible name to the visibility switch

This switch is rendered without an associated <label>/aria-label, so assistive technologies will announce an unlabeled control and users cannot tell it changes agent visibility. The previous implementation had an explicit label, so this introduces an accessibility regression for screen-reader users in the create/edit form.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
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: 2

Caution

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

⚠️ Outside diff range comments (1)
components/Agents/AgentEditDialog.tsx (1)

46-62: ⚠️ Potential issue | 🟠 Major

Clear shareEmails when the agent is saved as public.

finalShareEmails ignores values.isPrivate, so flipping a private agent to public still PATCHes currentSharedEmails. That leaves stale sharing metadata attached to a public template.

🧹 Suggested fix
-      const finalShareEmails = values.shareEmails && values.shareEmails.length > 0
-        ? [...currentSharedEmails, ...values.shareEmails]
-        : currentSharedEmails;
+      const finalShareEmails = values.isPrivate
+        ? [...currentSharedEmails, ...(values.shareEmails ?? [])]
+        : [];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Agents/AgentEditDialog.tsx` around lines 46 - 62, finalShareEmails
currently concatenates currentSharedEmails with values.shareEmails regardless of
privacy state, so when values.isPrivate is false (agent made public) stale
currentSharedEmails are still sent; update the logic in AgentEditDialog where
finalShareEmails is computed to clear shareEmails when values.isPrivate is false
(i.e., set finalShareEmails = [] if !values.isPrivate), otherwise preserve the
existing merge behavior (using currentSharedEmails and values.shareEmails), and
ensure the PATCH body uses this corrected finalShareEmails field.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/Agents/Agents.tsx`:
- Around line 38-66: Wrap the two visibility buttons in an accessible group
(e.g., add role="group" and an aria-label or aria-labelledby) and mark each
toggle button with aria-pressed reflecting the current state so assistive tech
can announce which is active; specifically, update the Public and Private button
elements in Agents.tsx to include aria-pressed={!isPrivate} for the Public
button and aria-pressed={isPrivate} for the Private button (keep existing
onClick handlers like togglePrivate and the isPrivate state logic intact) and
ensure the container element has a clear aria-label describing the control.

In `@components/Agents/AgentVisibilityControl.tsx`:
- Around line 13-34: The Switch with id "isPrivate" is currently unlabeled for
assistive tech; give it an accessible name that reflects the Public/Private
mapping by either setting aria-label dynamically (e.g., "Private" when checked,
"Public" when unchecked) or wire aria-labelledby to one of the visible spans and
ensure that the labeled span text updates with the isPrivate state; update the
Switch props (id "isPrivate", checked={isPrivate}) to include that aria
attribute so screen readers announce the correct state while keeping the
existing form.setValue handler intact.

---

Outside diff comments:
In `@components/Agents/AgentEditDialog.tsx`:
- Around line 46-62: finalShareEmails currently concatenates currentSharedEmails
with values.shareEmails regardless of privacy state, so when values.isPrivate is
false (agent made public) stale currentSharedEmails are still sent; update the
logic in AgentEditDialog where finalShareEmails is computed to clear shareEmails
when values.isPrivate is false (i.e., set finalShareEmails = [] if
!values.isPrivate), otherwise preserve the existing merge behavior (using
currentSharedEmails and values.shareEmails), and ensure the PATCH body uses this
corrected finalShareEmails field.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 20f5cec7-8619-4c70-b4ce-7aae99778032

📥 Commits

Reviewing files that changed from the base of the PR and between 0a6fc82 and a04a94f.

📒 Files selected for processing (8)
  • components/Agents/AgentEditDialog.tsx
  • components/Agents/AgentVisibilityControl.tsx
  • components/Agents/Agents.tsx
  • components/Agents/CreateAgentDialog.tsx
  • components/Agents/CreateAgentForm.tsx
  • components/Agents/PrivacySection.tsx
  • components/Agents/TagSelector.tsx
  • components/Agents/useAgentForm.ts

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
recoup-chat Ready Ready Preview Mar 31, 2026 3:23am

Request Review

Comment on lines +39 to +66
<button
type="button"
onClick={() => {
if (isPrivate) togglePrivate();
}}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium transition-all cursor-pointer",
!isPrivate
? "bg-background text-foreground shadow"
: "text-muted-foreground hover:text-foreground"
)}
>
Public
</button>
<button
type="button"
onClick={() => {
if (!isPrivate) togglePrivate();
}}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium transition-all cursor-pointer",
isPrivate
? "bg-background text-foreground shadow"
: "text-muted-foreground hover:text-foreground"
)}
>
Private
</button>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

SRP / OCP

  • actual: button component defined within general Agents component.
  • required: new button component file.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Understood. I’ll extract that button into its own component file.

Comment on lines +27 to +29
<p className="text-sm text-muted-foreground">
Add email addresses for the people who should be able to view this private agent.
</p>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Open question: Why add this in PrivacySection instead of within EmailShareInput?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, fair point. I placed it in PrivacySection because I treated that component as the container for all private-sharing behavior, but I agree the more specific ownership is EmailShareInput. I’ll move it there.

import { useAgentData } from "./useAgentData";
import { CreateAgentFormData } from "./schemas";

const FALLBACK_TAGS = [
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why are you adding Fallback tags?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I added the fallback tags to preserve the category options locally when the backing tag data was sparse, mainly so the modal remained testable and closer to hosted behavior during local UI work. That said, I agree this introduces hardcoded behavior in the UI, so I’ll remove it from this PR.

Copy link
Copy Markdown
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.

🧹 Nitpick comments (5)
components/Agents/PrivacySection.tsx (1)

26-33: Minor: Inconsistent JSX indentation.

The EmailShareInput component and its props have extra leading spaces compared to the surrounding JSX. While functionally correct, aligning the indentation would improve readability.

✨ Aligned indentation
      {isPrivate && (
        <EmailShareInput
-            emails={form.watch("shareEmails") ?? []}
-            existingSharedEmails={existingSharedEmails}
-            onEmailsChange={(emails) => {
-              form.setValue("shareEmails", emails, { shouldDirty: true, shouldValidate: true });
-            }}
-            onExistingEmailsChange={onExistingEmailsChange}
-          />
+          emails={form.watch("shareEmails") ?? []}
+          existingSharedEmails={existingSharedEmails}
+          onEmailsChange={(emails) => {
+            form.setValue("shareEmails", emails, { shouldDirty: true, shouldValidate: true });
+          }}
+          onExistingEmailsChange={onExistingEmailsChange}
+        />
      )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Agents/PrivacySection.tsx` around lines 26 - 33, The JSX for
EmailShareInput is misaligned with extra leading spaces; open the component
block containing EmailShareInput and normalize indentation so the tag and all
its props (EmailShareInput, emails prop using form.watch("shareEmails"),
existingSharedEmails, onEmailsChange handler that calls form.setValue(...), and
onExistingEmailsChange) align with the surrounding JSX structure—ensure each
prop is indented consistently (same level as neighboring components) for
readable, consistent formatting.
hooks/useAgentForm.ts (1)

24-30: Simplify the useEffect - the else-if branch is unreachable.

Given that defaultValues.shareEmails is always initialized to initialValues?.shareEmails ?? [] on line 18, form.getValues("shareEmails") will never be falsy. The condition on line 27 is effectively dead code.

♻️ Simplified effect
  useEffect(() => {
    if (!isPrivate) {
      form.setValue("shareEmails", []);
-   } else if (!form.getValues("shareEmails")) {
-     form.setValue("shareEmails", []);
    }
  }, [isPrivate, form]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/useAgentForm.ts` around lines 24 - 30, The else-if branch in the
useEffect is unreachable because defaultValues.shareEmails is always initialized
(via initialValues?.shareEmails ?? []), so simplify the effect: keep the check
on isPrivate and call form.setValue("shareEmails", []) when !isPrivate, and
remove the redundant else-if that checks form.getValues("shareEmails"); update
the useEffect that references isPrivate, form.setValue, and form.getValues
accordingly.
hooks/useEditAgentTemplate.ts (2)

22-24: Consider deduplicating emails before sending to API.

The merge of currentSharedEmails and values.shareEmails could theoretically produce duplicates if the same email appears in both arrays. While the current UI flow makes this unlikely (per context, form always resets shareEmails to []), adding deduplication would be a defensive measure against future edge cases.

♻️ Optional deduplication
      const finalShareEmails = values.isPrivate
-       ? [...currentSharedEmails, ...(values.shareEmails ?? [])]
+       ? [...new Set([...currentSharedEmails, ...(values.shareEmails ?? [])])]
        : [];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/useEditAgentTemplate.ts` around lines 22 - 24, The merged email array
assigned to finalShareEmails in useEditAgentTemplate.ts can contain duplicates
from currentSharedEmails and values.shareEmails; update the assignment so when
values.isPrivate is true you deduplicate the combined list (e.g., use a Set or
filter to preserve first-occurrence order) before sending to the API, while
keeping the existing isPrivate branch behavior (and ensure the deduplication
happens only when computing finalShareEmails).

29-31: Consider guarding against undefined userId before making the request.

If userData hasn't loaded yet, userId will be undefined, and the API will reject with a 400 error (per the PATCH handler at app/api/agent-templates/route.ts:154-156). While the mutation will throw and error handling will catch it, the user experience would be clearer with an early guard or by disabling the submit action until user data is available.

🛡️ Proposed early validation
  return useMutation({
    mutationFn: async (values: CreateAgentFormData) => {
+     if (!userData?.id) {
+       throw new Error("User not authenticated");
+     }
+
      const finalShareEmails = values.isPrivate
        ? [...currentSharedEmails, ...(values.shareEmails ?? [])]
        : [];

      const res = await fetch("/api/agent-templates", {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          id: agent.id,
-         userId: userData?.id,
+         userId: userData.id,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/useEditAgentTemplate.ts` around lines 29 - 31, The request body may
include an undefined userId when userData hasn't loaded; update the mutation in
useEditAgentTemplate (the code that builds body: JSON.stringify({ id: agent.id,
userId: userData?.id, ... })) to guard early: if userData?.id is falsy, abort
the request and surface a clear error (or return early) so no PATCH is sent with
undefined userId; alternatively ensure the submit action is disabled until
userData is present—change the check in the mutation handler that constructs the
body to validate userData?.id and handle the missing user id before calling
fetch.
components/Agents/AgentsVisibilityFilter.tsx (1)

3-11: Refactor to a value-change API to support controlled + uncontrolled usage.

The current togglePrivate contract couples this component to flip semantics and forces click guards in the view layer. Prefer a standard value/defaultValue + onValueChange(next) shape so the component can be reused cleanly in both controlled and uncontrolled modes.

♻️ Proposed refactor
+import { useState } from "react";
 import { cn } from "@/lib/utils";

 interface AgentsVisibilityFilterProps {
-  isPrivate: boolean;
-  togglePrivate: () => void;
+  isPrivate?: boolean;
+  defaultIsPrivate?: boolean;
+  onIsPrivateChange?: (next: boolean) => void;
 }

 const AgentsVisibilityFilter = ({
-  isPrivate,
-  togglePrivate,
+  isPrivate: controlledIsPrivate,
+  defaultIsPrivate = false,
+  onIsPrivateChange,
 }: AgentsVisibilityFilterProps) => {
+  const [uncontrolledIsPrivate, setUncontrolledIsPrivate] = useState(defaultIsPrivate);
+  const isPrivate = controlledIsPrivate ?? uncontrolledIsPrivate;
+  const setIsPrivate = (next: boolean) => {
+    if (controlledIsPrivate === undefined) setUncontrolledIsPrivate(next);
+    onIsPrivateChange?.(next);
+  };
+
   return (
     <div
       role="group"
       aria-label="Agent visibility filter"
       className="inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground"
     >
       <button
         type="button"
         aria-pressed={!isPrivate}
-        onClick={() => {
-          if (isPrivate) togglePrivate();
-        }}
+        onClick={() => setIsPrivate(false)}
         className={cn(
           "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium transition-all cursor-pointer",
           !isPrivate
             ? "bg-background text-foreground shadow"
             : "text-muted-foreground hover:text-foreground"
         )}
       >
         Public
       </button>
       <button
         type="button"
         aria-pressed={isPrivate}
-        onClick={() => {
-          if (!isPrivate) togglePrivate();
-        }}
+        onClick={() => setIsPrivate(true)}
         className={cn(
           "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium transition-all cursor-pointer",
           isPrivate
             ? "bg-background text-foreground shadow"
             : "text-muted-foreground hover:text-foreground"
         )}
       >
         Private
       </button>
     </div>
   );
 };

As per coding guidelines: "Support both controlled and uncontrolled state in components" and "Provide defaultValue and onValueChange props for state management."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/Agents/AgentsVisibilityFilter.tsx` around lines 3 - 11, Change the
component API from isPrivate + togglePrivate to a value/defaultValue +
onValueChange(next: boolean) shape: update the AgentsVisibilityFilterProps to
accept value?: boolean, defaultValue?: boolean, and onValueChange?: (next:
boolean) => void, then inside AgentsVisibilityFilter use the controlled pattern
(use the passed value when defined otherwise keep internal state initialized
from defaultValue) and call onValueChange(next) whenever the visibility changes;
replace any togglePrivate usages/click handlers to compute the next boolean and
call onValueChange(next) (or update internal state when uncontrolled).
Reference: AgentsVisibilityFilter and AgentsVisibilityFilterProps (rename
togglePrivate → onValueChange, isPrivate → value/defaultValue) and ensure event
handlers use the new onValueChange(next) contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@components/Agents/AgentsVisibilityFilter.tsx`:
- Around line 3-11: Change the component API from isPrivate + togglePrivate to a
value/defaultValue + onValueChange(next: boolean) shape: update the
AgentsVisibilityFilterProps to accept value?: boolean, defaultValue?: boolean,
and onValueChange?: (next: boolean) => void, then inside AgentsVisibilityFilter
use the controlled pattern (use the passed value when defined otherwise keep
internal state initialized from defaultValue) and call onValueChange(next)
whenever the visibility changes; replace any togglePrivate usages/click handlers
to compute the next boolean and call onValueChange(next) (or update internal
state when uncontrolled). Reference: AgentsVisibilityFilter and
AgentsVisibilityFilterProps (rename togglePrivate → onValueChange, isPrivate →
value/defaultValue) and ensure event handlers use the new onValueChange(next)
contract.

In `@components/Agents/PrivacySection.tsx`:
- Around line 26-33: The JSX for EmailShareInput is misaligned with extra
leading spaces; open the component block containing EmailShareInput and
normalize indentation so the tag and all its props (EmailShareInput, emails prop
using form.watch("shareEmails"), existingSharedEmails, onEmailsChange handler
that calls form.setValue(...), and onExistingEmailsChange) align with the
surrounding JSX structure—ensure each prop is indented consistently (same level
as neighboring components) for readable, consistent formatting.

In `@hooks/useAgentForm.ts`:
- Around line 24-30: The else-if branch in the useEffect is unreachable because
defaultValues.shareEmails is always initialized (via initialValues?.shareEmails
?? []), so simplify the effect: keep the check on isPrivate and call
form.setValue("shareEmails", []) when !isPrivate, and remove the redundant
else-if that checks form.getValues("shareEmails"); update the useEffect that
references isPrivate, form.setValue, and form.getValues accordingly.

In `@hooks/useEditAgentTemplate.ts`:
- Around line 22-24: The merged email array assigned to finalShareEmails in
useEditAgentTemplate.ts can contain duplicates from currentSharedEmails and
values.shareEmails; update the assignment so when values.isPrivate is true you
deduplicate the combined list (e.g., use a Set or filter to preserve
first-occurrence order) before sending to the API, while keeping the existing
isPrivate branch behavior (and ensure the deduplication happens only when
computing finalShareEmails).
- Around line 29-31: The request body may include an undefined userId when
userData hasn't loaded; update the mutation in useEditAgentTemplate (the code
that builds body: JSON.stringify({ id: agent.id, userId: userData?.id, ... }))
to guard early: if userData?.id is falsy, abort the request and surface a clear
error (or return early) so no PATCH is sent with undefined userId; alternatively
ensure the submit action is disabled until userData is present—change the check
in the mutation handler that constructs the body to validate userData?.id and
handle the missing user id before calling fetch.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 917c403c-2831-47b4-9378-5e49ea309f67

📥 Commits

Reviewing files that changed from the base of the PR and between a04a94f and 2a2d670.

📒 Files selected for processing (10)
  • components/Agents/AgentEditDialog.tsx
  • components/Agents/AgentVisibilityControl.tsx
  • components/Agents/Agents.tsx
  • components/Agents/AgentsVisibilityFilter.tsx
  • components/Agents/CreateAgentDialog.tsx
  • components/Agents/EmailShareInput.tsx
  • components/Agents/PrivacySection.tsx
  • components/Agents/TagSelector.tsx
  • hooks/useAgentForm.ts
  • hooks/useEditAgentTemplate.ts
✅ Files skipped from review due to trivial changes (4)
  • components/Agents/EmailShareInput.tsx
  • components/Agents/TagSelector.tsx
  • components/Agents/Agents.tsx
  • components/Agents/AgentVisibilityControl.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • components/Agents/CreateAgentDialog.tsx
  • components/Agents/AgentEditDialog.tsx

@sweetmantech sweetmantech changed the base branch from main to test March 31, 2026 03:16
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e2bf71e1a3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +41 to +43
const emailAddresses = accountEmails
.map((row) => row.email)
.filter((email): email is string => typeof email === "string" && email.length > 0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize invited-share email lookups

Invited emails are normalized to lowercase when they are stored (splitShareEmailsByAccount), but this code builds emailAddresses from raw account_emails.email values and later uses that array for the agent_template_email_shares lookup. If an account email is stored with mixed case, it will not match a lowercase invited-share row, so the user can miss templates shared to their address. Normalize/trim these lookup emails before querying to avoid this access regression.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

6 issues found across 20 files

Confidence score: 2/5

  • High-risk issue: lib/supabase/agent_templates/insertAgentTemplateEmailShares.ts uses Tables<"agent_template_email_shares">["Insert"], but Tables<T> maps to Row types; this is likely a TypeScript breakage at compile time and is merge-blocking until corrected.
  • lib/supabase/agent_templates/getSharedTemplatesForUser.ts has concrete behavior risk from case-sensitive email matching; without lowercasing emailAddresses, invited shares can be missed for users with mixed-case emails.
  • Other findings are lower impact but worth cleanup before/soon after merge: duplicated processing logic in getSharedTemplatesForUser.ts, a console.error convention violation in lib/supabase/agent_templates/getAgentTemplateEmailSharesByTemplateIds.ts, and an unreachable branch in hooks/useAgentForm.ts.
  • Pay close attention to lib/supabase/agent_templates/insertAgentTemplateEmailShares.ts and lib/supabase/agent_templates/getSharedTemplatesForUser.ts - type correctness and email normalization can directly affect build stability and share visibility.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lib/supabase/agent_templates/getSharedTemplatesForUser.ts">

<violation number="1" location="lib/supabase/agent_templates/getSharedTemplatesForUser.ts:40">
P2: The `getAccountEmails(userId)` call is independent of the first `agent_template_shares` query — both only need `userId`. Running them in parallel with `Promise.all` would reduce total latency by overlapping the two network round-trips.</violation>

<violation number="2" location="lib/supabase/agent_templates/getSharedTemplatesForUser.ts:42">
P2: Normalize `emailAddresses` to lowercase before the `agent_template_email_shares` lookup. Invited-share emails are stored lowercased (via `splitShareEmailsByAccount`), but `account_emails.email` values are used here without normalization. If an account email has mixed case, the `.in("email", emailAddresses)` query will miss matching invited-share rows, causing shared templates to silently not appear for that user.</violation>

<violation number="3" location="lib/supabase/agent_templates/getSharedTemplatesForUser.ts:62">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The `emailShareData?.forEach(...)` block is a verbatim copy of the existing `data?.forEach(...)` processing loop. Extract a shared helper (e.g. `collectTemplates(shares, templates, processedIds)`) to eliminate the duplication.</violation>
</file>

<file name="lib/supabase/agent_templates/insertAgentTemplateEmailShares.ts">

<violation number="1" location="lib/supabase/agent_templates/insertAgentTemplateEmailShares.ts:5">
P1: Custom agent: **Enforce Clear Code Style and Maintainability Practices**

`Tables<"agent_template_email_shares">["Insert"]` is incorrect — `Tables<T>` resolves to the Row type, which has no `Insert` member. This will either fail to compile or silently produce `never`, making the function unusable. Use `TablesInsert<"agent_template_email_shares">` instead.</violation>
</file>

<file name="lib/supabase/agent_templates/getAgentTemplateEmailSharesByTemplateIds.ts">

<violation number="1" location="lib/supabase/agent_templates/getAgentTemplateEmailSharesByTemplateIds.ts:19">
P2: Remove `console.error` before merging — the project conventions prohibit production logging (`No Production Logging` in CLAUDE.md), and no other file in `lib/supabase/` uses console statements. The error is already re-thrown, so callers can handle it.</violation>
</file>

<file name="hooks/useAgentForm.ts">

<violation number="1" location="hooks/useAgentForm.ts:27">
P3: The `else if` branch is unreachable: `shareEmails` is always initialized to an array (from `defaultValues` or the Zod `.default([])`), and `![]` is `false` in JavaScript. The entire `else if` block can be removed since the only meaningful work here is clearing emails when toggling to public.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

import type { Tables } from "@/types/database.types";

type AgentTemplateEmailShare = Tables<"agent_template_email_shares">;
type AgentTemplateEmailShareInsert = Tables<"agent_template_email_shares">["Insert"];
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 5, 2026

Choose a reason for hiding this comment

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

P1: Custom agent: Enforce Clear Code Style and Maintainability Practices

Tables<"agent_template_email_shares">["Insert"] is incorrect — Tables<T> resolves to the Row type, which has no Insert member. This will either fail to compile or silently produce never, making the function unusable. Use TablesInsert<"agent_template_email_shares"> instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/supabase/agent_templates/insertAgentTemplateEmailShares.ts, line 5:

<comment>`Tables<"agent_template_email_shares">["Insert"]` is incorrect — `Tables<T>` resolves to the Row type, which has no `Insert` member. This will either fail to compile or silently produce `never`, making the function unusable. Use `TablesInsert<"agent_template_email_shares">` instead.</comment>

<file context>
@@ -0,0 +1,28 @@
+import type { Tables } from "@/types/database.types";
+
+type AgentTemplateEmailShare = Tables<"agent_template_email_shares">;
+type AgentTemplateEmailShareInsert = Tables<"agent_template_email_shares">["Insert"];
+
+export async function insertAgentTemplateEmailShares(
</file context>
Fix with Cubic

throw emailShareError;
}

emailShareData?.forEach((share: SharedTemplateData) => {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 5, 2026

Choose a reason for hiding this comment

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

P2: Custom agent: Flag AI Slop and Fabricated Changes

The emailShareData?.forEach(...) block is a verbatim copy of the existing data?.forEach(...) processing loop. Extract a shared helper (e.g. collectTemplates(shares, templates, processedIds)) to eliminate the duplication.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/supabase/agent_templates/getSharedTemplatesForUser.ts, line 62:

<comment>The `emailShareData?.forEach(...)` block is a verbatim copy of the existing `data?.forEach(...)` processing loop. Extract a shared helper (e.g. `collectTemplates(shares, templates, processedIds)`) to eliminate the duplication.</comment>

<file context>
@@ -36,5 +37,42 @@ export async function getSharedTemplatesForUser(userId: string): Promise<AgentTe
+    throw emailShareError;
+  }
+
+  emailShareData?.forEach((share: SharedTemplateData) => {
+    if (!share || !share.templates) return;
+
</file context>
Fix with Cubic

.in("template_id", templateIds);

if (error) {
console.error("Error fetching agent template email shares:", error);
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 5, 2026

Choose a reason for hiding this comment

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

P2: Remove console.error before merging — the project conventions prohibit production logging (No Production Logging in CLAUDE.md), and no other file in lib/supabase/ uses console statements. The error is already re-thrown, so callers can handle it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/supabase/agent_templates/getAgentTemplateEmailSharesByTemplateIds.ts, line 19:

<comment>Remove `console.error` before merging — the project conventions prohibit production logging (`No Production Logging` in CLAUDE.md), and no other file in `lib/supabase/` uses console statements. The error is already re-thrown, so callers can handle it.</comment>

<file context>
@@ -0,0 +1,24 @@
+    .in("template_id", templateIds);
+
+  if (error) {
+    console.error("Error fetching agent template email shares:", error);
+    throw error;
+  }
</file context>
Fix with Cubic

});
});

const accountEmails = await getAccountEmails(userId);
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 5, 2026

Choose a reason for hiding this comment

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

P2: The getAccountEmails(userId) call is independent of the first agent_template_shares query — both only need userId. Running them in parallel with Promise.all would reduce total latency by overlapping the two network round-trips.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/supabase/agent_templates/getSharedTemplatesForUser.ts, line 40:

<comment>The `getAccountEmails(userId)` call is independent of the first `agent_template_shares` query — both only need `userId`. Running them in parallel with `Promise.all` would reduce total latency by overlapping the two network round-trips.</comment>

<file context>
@@ -36,5 +37,42 @@ export async function getSharedTemplatesForUser(userId: string): Promise<AgentTe
     });
   });
 
+  const accountEmails = await getAccountEmails(userId);
+  const emailAddresses = accountEmails
+    .map((row) => row.email)
</file context>
Fix with Cubic


const accountEmails = await getAccountEmails(userId);
const emailAddresses = accountEmails
.map((row) => row.email)
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 5, 2026

Choose a reason for hiding this comment

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

P2: Normalize emailAddresses to lowercase before the agent_template_email_shares lookup. Invited-share emails are stored lowercased (via splitShareEmailsByAccount), but account_emails.email values are used here without normalization. If an account email has mixed case, the .in("email", emailAddresses) query will miss matching invited-share rows, causing shared templates to silently not appear for that user.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/supabase/agent_templates/getSharedTemplatesForUser.ts, line 42:

<comment>Normalize `emailAddresses` to lowercase before the `agent_template_email_shares` lookup. Invited-share emails are stored lowercased (via `splitShareEmailsByAccount`), but `account_emails.email` values are used here without normalization. If an account email has mixed case, the `.in("email", emailAddresses)` query will miss matching invited-share rows, causing shared templates to silently not appear for that user.</comment>

<file context>
@@ -36,5 +37,42 @@ export async function getSharedTemplatesForUser(userId: string): Promise<AgentTe
 
+  const accountEmails = await getAccountEmails(userId);
+  const emailAddresses = accountEmails
+    .map((row) => row.email)
+    .filter((email): email is string => typeof email === "string" && email.length > 0);
+
</file context>
Suggested change
.map((row) => row.email)
.map((row) => row.email?.trim().toLowerCase())
Fix with Cubic

useEffect(() => {
if (!isPrivate) {
form.setValue("shareEmails", []);
} else if (!form.getValues("shareEmails")) {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 5, 2026

Choose a reason for hiding this comment

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

P3: The else if branch is unreachable: shareEmails is always initialized to an array (from defaultValues or the Zod .default([])), and ![] is false in JavaScript. The entire else if block can be removed since the only meaningful work here is clearing emails when toggling to public.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At hooks/useAgentForm.ts, line 27:

<comment>The `else if` branch is unreachable: `shareEmails` is always initialized to an array (from `defaultValues` or the Zod `.default([])`), and `![]` is `false` in JavaScript. The entire `else if` block can be removed since the only meaningful work here is clearing emails when toggling to public.</comment>

<file context>
@@ -0,0 +1,33 @@
+  useEffect(() => {
+    if (!isPrivate) {
+      form.setValue("shareEmails", []);
+    } else if (!form.getValues("shareEmails")) {
+      form.setValue("shareEmails", []);
+    }
</file context>
Fix with Cubic

Copy link
Copy Markdown
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: 2

🧹 Nitpick comments (3)
lib/supabase/agent_templates/getSharedEmailsForTemplates.ts (1)

9-14: Split the map-merge step into a helper.

This utility now fetches, expands account emails, merges invited emails, and dedupes in one place. Pulling the emailMap append logic into a helper would keep it within the repo's utility-size guideline and make the two share sources easier to test separately. As per coding guidelines, "Keep functions under 50 lines" and "Each function should have a single responsibility and clear naming".

Also applies to: 46-52

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/supabase/agent_templates/getSharedEmailsForTemplates.ts` around lines 9 -
14, The function getSharedEmailsForTemplates currently builds and merges
emailMap inline after calling getAgentTemplateSharesByTemplateIds and
getAgentTemplateEmailSharesByTemplateIds; extract that merge/dedupe/append logic
into a small helper (e.g., buildEmailMapFromShares or mergeTemplateEmailShares)
so getSharedEmailsForTemplates only orchestrates fetching and calls the helper
to produce the final map. The helper should accept the two arrays (shares and
invitedEmailShares), expand account emails, merge invited emails, dedupe
entries, and return the emailMap; update getSharedEmailsForTemplates to call
this helper and keep its body under 50 lines with a single responsibility.
lib/supabase/agent_templates/getSharedTemplatesForUser.ts (2)

62-75: Extract the repeated template merge block.

The new email-share branch duplicates the same array-normalization/dedupe-by-id loop used above, and this utility is now well past the repo's utility size guideline. A small appendUniqueTemplates helper would keep the two paths aligned. As per coding guidelines, "Keep functions under 50 lines" and "DRY: Consolidate similar logic into shared utilities".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/supabase/agent_templates/getSharedTemplatesForUser.ts` around lines 62 -
75, The loop that normalizes share.templates to an array and deduplicates by id
is duplicated (see emailShareData processing using SharedTemplateData,
AgentTemplateRow, processedIds, templates); extract that logic into a small
helper (e.g., appendUniqueTemplates) that accepts the incoming templates (single
item or array), the destination array (templates) and the processedIds Set,
performs array-normalization and id-based dedupe, and replace the duplicated
block with a call to this helper from both places so the logic is consolidated
and functions stay under 50 lines.

40-43: Normalize the lookup emails before querying invite shares.

The invite write path lowercases stored emails, but this branch queries with raw account_emails.email values. Normalizing and deduping here avoids a casing-dependent lookup.

♻️ Suggested normalization
-  const emailAddresses = accountEmails
-    .map((row) => row.email)
-    .filter((email): email is string => typeof email === "string" && email.length > 0);
+  const emailAddresses = [
+    ...new Set(
+      accountEmails
+        .map((row) => row.email?.trim().toLowerCase())
+        .filter((email): email is string => typeof email === "string" && email.length > 0)
+    ),
+  ];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/supabase/agent_templates/getSharedTemplatesForUser.ts` around lines 40 -
43, The email list from getAccountEmails (accountEmails -> emailAddresses) must
be normalized and deduped before being used to query invite shares: convert each
email to lowercase (and trim), keep the existing type guard/length check, and
remove duplicates (e.g., via a Set) so lookups match the stored lowercased
invite emails; update the code around emailAddresses in
getSharedTemplatesForUser.ts to perform these steps and use the normalized
deduped list in the subsequent query.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/supabase/agent_templates/splitShareEmailsByAccount.ts`:
- Around line 11-15: The code currently assumes every item in the emails array
is a string and calls trim()/toLowerCase(), which can throw or persist bad
values; update the logic that builds normalizedEmails (and the similar block at
the other occurrence) to first filter entries to only typeof === "string" and
non-empty after trimming, and then validate each trimmed string with an email
validation check (e.g., a simple regex or shared isValidEmail utility) before
lowercasing and deduplicating; ensure both the normalizedEmails variable and the
second mapping/filtering block use this validation so only well-formed emails
are written as invites.

In `@lib/supabase/agent_templates/updateAgentTemplateShares.ts`:
- Around line 16-44: The current code deletes shares via
deleteAgentTemplateSharesByTemplateId and
deleteAgentTemplateEmailSharesByTemplateId before validating/inserting new
recipients from splitShareEmailsByAccount and then calling
insertAgentTemplateShares and insertAgentTemplateEmailShares; change this to an
atomic replace by performing the delete+insert inside a single database
transaction or by calling a DB-side RPC/function that does the replace (e.g.,
create an RPC like replace_agent_template_shares(template_id, emails) or wrap
the existing operations in a transaction), so that either all deletions and
insertions succeed or none are applied; update the caller to use that
transactional RPC/transaction and remove the separate delete/update sequence.

---

Nitpick comments:
In `@lib/supabase/agent_templates/getSharedEmailsForTemplates.ts`:
- Around line 9-14: The function getSharedEmailsForTemplates currently builds
and merges emailMap inline after calling getAgentTemplateSharesByTemplateIds and
getAgentTemplateEmailSharesByTemplateIds; extract that merge/dedupe/append logic
into a small helper (e.g., buildEmailMapFromShares or mergeTemplateEmailShares)
so getSharedEmailsForTemplates only orchestrates fetching and calls the helper
to produce the final map. The helper should accept the two arrays (shares and
invitedEmailShares), expand account emails, merge invited emails, dedupe
entries, and return the emailMap; update getSharedEmailsForTemplates to call
this helper and keep its body under 50 lines with a single responsibility.

In `@lib/supabase/agent_templates/getSharedTemplatesForUser.ts`:
- Around line 62-75: The loop that normalizes share.templates to an array and
deduplicates by id is duplicated (see emailShareData processing using
SharedTemplateData, AgentTemplateRow, processedIds, templates); extract that
logic into a small helper (e.g., appendUniqueTemplates) that accepts the
incoming templates (single item or array), the destination array (templates) and
the processedIds Set, performs array-normalization and id-based dedupe, and
replace the duplicated block with a call to this helper from both places so the
logic is consolidated and functions stay under 50 lines.
- Around line 40-43: The email list from getAccountEmails (accountEmails ->
emailAddresses) must be normalized and deduped before being used to query invite
shares: convert each email to lowercase (and trim), keep the existing type
guard/length check, and remove duplicates (e.g., via a Set) so lookups match the
stored lowercased invite emails; update the code around emailAddresses in
getSharedTemplatesForUser.ts to perform these steps and use the normalized
deduped list in the subsequent query.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a490409f-9c98-453e-9a7f-2be3a87b7e0f

📥 Commits

Reviewing files that changed from the base of the PR and between 2a2d670 and e2bf71e.

⛔ Files ignored due to path filters (1)
  • types/database.types.ts is excluded by none and included by none
📒 Files selected for processing (9)
  • components/Agents/AgentEditDialog.tsx
  • lib/supabase/agent_templates/createAgentTemplateShares.ts
  • lib/supabase/agent_templates/deleteAgentTemplateEmailShares.ts
  • lib/supabase/agent_templates/getAgentTemplateEmailSharesByTemplateIds.ts
  • lib/supabase/agent_templates/getSharedEmailsForTemplates.ts
  • lib/supabase/agent_templates/getSharedTemplatesForUser.ts
  • lib/supabase/agent_templates/insertAgentTemplateEmailShares.ts
  • lib/supabase/agent_templates/splitShareEmailsByAccount.ts
  • lib/supabase/agent_templates/updateAgentTemplateShares.ts

Comment on lines +11 to +15
const normalizedEmails = [...new Set(
emails
.map((email) => email.trim().toLowerCase())
.filter(Boolean)
)];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate each input before treating it as an invited email.

This code assumes every array entry is a string and only trims/lowercases it. A malformed payload can throw on trim() or persist junk like "foo" into agent_template_email_shares, because unresolved values are now written back out as invites. As per coding guidelines, "Implement built-in security practices for authentication and data handling".

Also applies to: 38-42

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/supabase/agent_templates/splitShareEmailsByAccount.ts` around lines 11 -
15, The code currently assumes every item in the emails array is a string and
calls trim()/toLowerCase(), which can throw or persist bad values; update the
logic that builds normalizedEmails (and the similar block at the other
occurrence) to first filter entries to only typeof === "string" and non-empty
after trimming, and then validate each trimmed string with an email validation
check (e.g., a simple regex or shared isValidEmail utility) before lowercasing
and deduplicating; ensure both the normalizedEmails variable and the second
mapping/filtering block use this validation so only well-formed emails are
written as invites.

Comment on lines +16 to 44
// Replace both account-based shares and raw invited email shares.
await Promise.all([
deleteAgentTemplateSharesByTemplateId(templateId),
deleteAgentTemplateEmailSharesByTemplateId(templateId),
]);

// Then create new shares if emails provided
if (emails && emails.length > 0) {
// Get user accounts by email using utility function
const userEmails = await getAccountDetailsByEmails(emails);
if (!emails || emails.length === 0) {
return;
}

if (userEmails.length === 0) {
return;
}
const { accountIds, invitedEmails } = await splitShareEmailsByAccount(emails);

// Create share records for found users (filter out null account_ids)
const sharesData = userEmails
.filter(userEmail => userEmail.account_id !== null)
.map(userEmail => ({
if (accountIds.length > 0) {
await insertAgentTemplateShares(
accountIds.map((accountId) => ({
template_id: templateId,
user_id: userEmail.account_id!,
}));
user_id: accountId,
}))
);
}

// Insert shares using utility function
await insertAgentTemplateShares(sharesData);
if (invitedEmails.length > 0) {
await insertAgentTemplateEmailShares(
invitedEmails.map((email) => ({
template_id: templateId,
email,
}))
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Make this replacement atomic.

Both share tables are deleted before the new recipients are validated or inserted. If resolution or either insert fails, the template update has already succeeded and the previous share set is gone. This needs a transactional replace operation (for example a DB function/RPC), not separate delete/insert calls.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/supabase/agent_templates/updateAgentTemplateShares.ts` around lines 16 -
44, The current code deletes shares via deleteAgentTemplateSharesByTemplateId
and deleteAgentTemplateEmailSharesByTemplateId before validating/inserting new
recipients from splitShareEmailsByAccount and then calling
insertAgentTemplateShares and insertAgentTemplateEmailShares; change this to an
atomic replace by performing the delete+insert inside a single database
transaction or by calling a DB-side RPC/function that does the replace (e.g.,
create an RPC like replace_agent_template_shares(template_id, emails) or wrap
the existing operations in a transaction), so that either all deletions and
insertions succeed or none are applied; update the caller to use that
transactional RPC/transaction and remove the separate delete/update sequence.

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.

2 participants