Refine agent visibility controls and browsing filters#1609
Refine agent visibility controls and browsing filters#1609ivaavimusic wants to merge 5 commits intorecoupable:testfrom
Conversation
|
@ivaavimusic is attempting to deploy a commit to the Recoupable Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughRefactors agent form ownership into a reusable Changes
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ❌ 1❌ Failed checks (1 warning)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 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".
| useEffect(() => { | ||
| if (open) { | ||
| setCurrentSharedEmails(agent.shared_emails || []); | ||
| form.reset({ | ||
| title: agent.title, |
There was a problem hiding this comment.
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 👍 / 👎.
| <Switch | ||
| id="isPrivate" | ||
| checked={isPrivate} |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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 | 🟠 MajorClear
shareEmailswhen the agent is saved as public.
finalShareEmailsignoresvalues.isPrivate, so flipping a private agent to public still PATCHescurrentSharedEmails. 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
📒 Files selected for processing (8)
components/Agents/AgentEditDialog.tsxcomponents/Agents/AgentVisibilityControl.tsxcomponents/Agents/Agents.tsxcomponents/Agents/CreateAgentDialog.tsxcomponents/Agents/CreateAgentForm.tsxcomponents/Agents/PrivacySection.tsxcomponents/Agents/TagSelector.tsxcomponents/Agents/useAgentForm.ts
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
components/Agents/Agents.tsx
Outdated
| <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> |
There was a problem hiding this comment.
SRP / OCP
- actual: button component defined within general Agents component.
- required: new button component file.
There was a problem hiding this comment.
Understood. I’ll extract that button into its own component file.
components/Agents/PrivacySection.tsx
Outdated
| <p className="text-sm text-muted-foreground"> | ||
| Add email addresses for the people who should be able to view this private agent. | ||
| </p> |
There was a problem hiding this comment.
Open question: Why add this in PrivacySection instead of within EmailShareInput?
There was a problem hiding this comment.
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.
components/Agents/TagSelector.tsx
Outdated
| import { useAgentData } from "./useAgentData"; | ||
| import { CreateAgentFormData } from "./schemas"; | ||
|
|
||
| const FALLBACK_TAGS = [ |
There was a problem hiding this comment.
Why are you adding Fallback tags?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
🧹 Nitpick comments (5)
components/Agents/PrivacySection.tsx (1)
26-33: Minor: Inconsistent JSX indentation.The
EmailShareInputcomponent 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.shareEmailsis always initialized toinitialValues?.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
currentSharedEmailsandvalues.shareEmailscould theoretically produce duplicates if the same email appears in both arrays. While the current UI flow makes this unlikely (per context, form always resetsshareEmailsto[]), 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 undefineduserIdbefore making the request.If
userDatahasn't loaded yet,userIdwill beundefined, and the API will reject with a 400 error (per the PATCH handler atapp/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
togglePrivatecontract couples this component to flip semantics and forces click guards in the view layer. Prefer a standardvalue/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
defaultValueandonValueChangeprops 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
📒 Files selected for processing (10)
components/Agents/AgentEditDialog.tsxcomponents/Agents/AgentVisibilityControl.tsxcomponents/Agents/Agents.tsxcomponents/Agents/AgentsVisibilityFilter.tsxcomponents/Agents/CreateAgentDialog.tsxcomponents/Agents/EmailShareInput.tsxcomponents/Agents/PrivacySection.tsxcomponents/Agents/TagSelector.tsxhooks/useAgentForm.tshooks/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
There was a problem hiding this comment.
💡 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".
| const emailAddresses = accountEmails | ||
| .map((row) => row.email) | ||
| .filter((email): email is string => typeof email === "string" && email.length > 0); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
6 issues found across 20 files
Confidence score: 2/5
- High-risk issue:
lib/supabase/agent_templates/insertAgentTemplateEmailShares.tsusesTables<"agent_template_email_shares">["Insert"], butTables<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.tshas concrete behavior risk from case-sensitive email matching; without lowercasingemailAddresses, 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, aconsole.errorconvention violation inlib/supabase/agent_templates/getAgentTemplateEmailSharesByTemplateIds.ts, and an unreachable branch inhooks/useAgentForm.ts. - Pay close attention to
lib/supabase/agent_templates/insertAgentTemplateEmailShares.tsandlib/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"]; |
There was a problem hiding this comment.
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>
| throw emailShareError; | ||
| } | ||
|
|
||
| emailShareData?.forEach((share: SharedTemplateData) => { |
There was a problem hiding this comment.
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>
| .in("template_id", templateIds); | ||
|
|
||
| if (error) { | ||
| console.error("Error fetching agent template email shares:", error); |
There was a problem hiding this comment.
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>
| }); | ||
| }); | ||
|
|
||
| const accountEmails = await getAccountEmails(userId); |
There was a problem hiding this comment.
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>
|
|
||
| const accountEmails = await getAccountEmails(userId); | ||
| const emailAddresses = accountEmails | ||
| .map((row) => row.email) |
There was a problem hiding this comment.
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>
| .map((row) => row.email) | |
| .map((row) => row.email?.trim().toLowerCase()) |
| useEffect(() => { | ||
| if (!isPrivate) { | ||
| form.setValue("shareEmails", []); | ||
| } else if (!form.getValues("shareEmails")) { |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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
emailMapappend 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
appendUniqueTemplateshelper 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.emailvalues. 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
⛔ Files ignored due to path filters (1)
types/database.types.tsis excluded by none and included by none
📒 Files selected for processing (9)
components/Agents/AgentEditDialog.tsxlib/supabase/agent_templates/createAgentTemplateShares.tslib/supabase/agent_templates/deleteAgentTemplateEmailShares.tslib/supabase/agent_templates/getAgentTemplateEmailSharesByTemplateIds.tslib/supabase/agent_templates/getSharedEmailsForTemplates.tslib/supabase/agent_templates/getSharedTemplatesForUser.tslib/supabase/agent_templates/insertAgentTemplateEmailShares.tslib/supabase/agent_templates/splitShareEmailsByAccount.tslib/supabase/agent_templates/updateAgentTemplateShares.ts
| const normalizedEmails = [...new Set( | ||
| emails | ||
| .map((email) => email.trim().toLowerCase()) | ||
| .filter(Boolean) | ||
| )]; |
There was a problem hiding this comment.
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.
| // 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, | ||
| })) | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.
Summary
This PR refines how public and private agent visibility is managed and viewed.
Changes
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
Improvements