Conversation
…atures - Added search functionality to the feed page, allowing users to filter rules by title, content, or author. - Implemented optional search filters in the hot and new API routes for fetching rules. - Introduced rule selection capabilities in the dashboard, enabling users to select multiple rules for actions like adding to lists or deleting. - Enhanced user experience with a dialog for adding selected rules to lists and improved feedback mechanisms for actions taken on rules.
…dependencies - Changed project name in package.json from "my-v0-project" to "cursor-link". - Updated README to reflect new features including Hot/New feeds, CLI tool capabilities, and lists management. - Enhanced tech stack section with updated versions for Next.js, TypeScript, Tailwind CSS, and other dependencies. - Added detailed sections for feed and discovery, lists management, and CLI tool usage.
|
@csark0812 is attempting to deploy a commit to the exon Team on Vercel. A member of the Team first needs to authorize it. |
WalkthroughAdds a tracked production env template and updates README. Implements search for Hot/New feeds (API and client). Enhances dashboard with multi-select, bulk add-to-list and bulk delete flows, and controlled create-list dialog. Adds Dashboard nav button, removes it from user menu, and renames package to "cursor-link." Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant FP as Feed Page (Client)
participant HF as /api/feed/hot
participant NF as /api/feed/new
participant DB as DB (cursorRule, user)
U->>FP: Type in search box
FP->>FP: Debounce 300ms
alt q present
FP->>HF: GET /api/feed/hot?q={q}
FP->>NF: GET /api/feed/new?q={q}
else no q
FP->>HF: GET /api/feed/hot
FP->>NF: GET /api/feed/new
end
HF->>DB: Select public rules with OR ilike on title/content/user.name (hot order)
DB-->>HF: Rows (≤50)
NF->>DB: Select public rules with OR ilike on title/content/user.name (newest)
DB-->>NF: Rows (≤50)
HF-->>FP: JSON results
NF-->>FP: JSON results
FP-->>U: Render Hot/New tabs (or “No results”)
sequenceDiagram
autonumber
actor U as User
participant DP as Dashboard Page
participant LS as /api/lists
participant LR as /api/lists/{listId}/rules
participant CR as /api/cursor-rules
U->>DP: Select multiple rules
U->>DP: Click "Add to List"
DP->>LS: GET /api/lists
LS-->>DP: Lists
U->>DP: Choose list
par For each selected rule
DP->>LR: POST /api/lists/{listId}/rules { ruleId }
LR-->>DP: 200 / error per rule
end
DP-->>U: Toast per-result, clear selection
U->>DP: Click "Delete Selected"
DP-->>U: Show confirm dialog
U->>DP: Confirm
par For each selected rule
DP->>CR: DELETE /api/cursor-rules?id={id}
CR-->>DP: 200 / error per rule
end
DP-->>U: Toast summary, update UI, clear selection
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
| <DialogTrigger asChild> | ||
| <Button | ||
| variant="primary" | ||
| size="sm" | ||
| className="bg-red-600 hover:bg-red-700 text-white" | ||
| onClick={handleDeleteSelected} | ||
| > | ||
| Delete {selectedRules.size} Rule{selectedRules.size !== 1 ? 's' : ''} | ||
| </Button> | ||
| </DialogTrigger> |
There was a problem hiding this comment.
The delete confirmation dialog uses DialogTrigger to wrap the delete button, which will close the dialog immediately when clicked, preventing the delete action from executing.
View Details
📝 Patch Details
diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx
index 971c6db..4bcb7c9 100644
--- a/app/dashboard/page.tsx
+++ b/app/dashboard/page.tsx
@@ -7,7 +7,7 @@ import { redirect } from "next/navigation"
import { useState, useEffect } from "react"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
-import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, DialogClose } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Card } from "@/components/ui/card"
import { track } from "@vercel/analytics"
@@ -207,6 +207,9 @@ export default function DashboardPage() {
}
}
+ // State for delete dialog
+ const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
+
// Handle deleting selected rules
const handleDeleteSelected = async () => {
if (selectedRules.size === 0) return
@@ -229,6 +232,7 @@ export default function DashboardPage() {
toast.success(`Deleted ${successful} rule${successful !== 1 ? 's' : ''}!`)
track("Rules Deleted", { count: successful })
clearSelection()
+ setIsDeleteDialogOpen(false) // Close dialog after successful deletion
}
} catch (error) {
console.error('Error deleting rules:', error)
@@ -553,7 +557,7 @@ export default function DashboardPage() {
Add to List
</button>
- <Dialog>
+ <Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<DialogTrigger asChild>
<button
disabled={selectedRules.size === 0}
@@ -584,21 +588,17 @@ export default function DashboardPage() {
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2">
- <DialogTrigger asChild>
- <Button variant="secondary" size="sm">
- Cancel
- </Button>
- </DialogTrigger>
- <DialogTrigger asChild>
- <Button
- variant="primary"
- size="sm"
- className="bg-red-600 hover:bg-red-700 text-white"
- onClick={handleDeleteSelected}
- >
- Delete {selectedRules.size} Rule{selectedRules.size !== 1 ? 's' : ''}
- </Button>
- </DialogTrigger>
+ <Button variant="secondary" size="sm" onClick={() => setIsDeleteDialogOpen(false)}>
+ Cancel
+ </Button>
+ <Button
+ variant="primary"
+ size="sm"
+ className="bg-red-600 hover:bg-red-700 text-white"
+ onClick={handleDeleteSelected}
+ >
+ Delete {selectedRules.size} Rule{selectedRules.size !== 1 ? 's' : ''}
+ </Button>
</DialogFooter>
</DialogContent>
</Dialog>
Analysis
The delete confirmation dialog has a UX bug where the delete button is wrapped with DialogTrigger asChild. This means when the user clicks "Delete X Rules", the dialog will close immediately due to the DialogTrigger behavior, and the onClick={handleDeleteSelected} may not execute properly or may execute after the dialog has closed.
The button structure is:
<DialogTrigger asChild>
<Button onClick={handleDeleteSelected}>
Delete {selectedRules.size} Rule{selectedRules.size !== 1 ? 's' : ''}
</Button>
</DialogTrigger>Expected behavior: User clicks delete button → confirmation executes → dialog closes after success
Actual behavior: User clicks delete button → dialog closes immediately → delete action may not complete properly
Fix: Remove the DialogTrigger asChild wrapper from the delete button and handle dialog closing manually in the handleDeleteSelected function, or use a different approach like DialogClose after the action completes.
The cancel button has the same pattern but it's correct behavior there since canceling should close the dialog.
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
components/auth/user-avatar.tsx (1)
179-181: Fix crash when email is undefined.
email?.charAt(0)can beundefined; calling.toUpperCase()then throws. Use a null-safe fallback.Apply this diff:
- <span className="text-white font-semibold text-sm leading-none"> - {session.user.email?.charAt(0).toUpperCase()} - </span> + <span className="text-white font-semibold text-sm leading-none"> + {(session.user.email?.charAt(0)?.toUpperCase() + ?? session.user.name?.charAt(0)?.toUpperCase() + ?? '')} + </span>components/dashboard/user-lists.tsx (1)
298-336: Single controlled Dialog to avoid duplicate modals.Both places share
open={isCreateDialogOpen}; whenlists.length === 0, opening one opens both. Use one Dialog root with two triggers or turn buttons intoonClicktoggles and keep a single Dialog content.Apply these diffs to convert triggers to simple buttons:
- <Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}> - <DialogTrigger asChild> - <Button variant="secondary" size="sm"> - <Plus className="h-4 w-4 mr-2" /> - Create List - </Button> - </DialogTrigger> - <DialogContent className="bg-[#1B1D21] border-white/10 text-white"> - ... - </DialogContent> - </Dialog> + <Button variant="secondary" size="sm" onClick={() => setIsCreateDialogOpen(true)}> + <Plus className="h-4 w-4 mr-2" /> + Create List + </Button>- <Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}> - <DialogTrigger asChild> - <Button variant="primary" size="sm"> - <Plus className="h-4 w-4 mr-2" /> - Create Your First List - </Button> - </DialogTrigger> - <DialogContent className="bg-[#1B1D21] border-white/10 text-white"> - ... - </DialogContent> - </Dialog> + <Button variant="primary" size="sm" onClick={() => setIsCreateDialogOpen(true)}> + <Plus className="h-4 w-4 mr-2" /> + Create Your First List + </Button>Add one shared Dialog once in the component (outside the lists/empty-state branches):
// Place once within the component's return (e.g., near the end of the top-level <div>) <Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}> <DialogContent className="bg-[#1B1D21] border-white/10 text-white"> <DialogHeader> <DialogTitle className="text-white">Create New List</DialogTitle> <DialogDescription className="text-gray-400"> Create a new list to organize your cursor rules </DialogDescription> </DialogHeader> <div className="space-y-4"> <Input placeholder="Enter list name" value={newListTitle} onChange={(e) => setNewListTitle(e.target.value)} className="bg-[#0F1419] border-white/10 text-white" onKeyDown={(e) => e.key === 'Enter' && handleCreateList()} /> </div> <DialogFooter> <Button variant="outline" className="border-white/10 bg-transparent hover:bg-white/5" onClick={() => setIsCreateDialogOpen(false)}> Cancel </Button> <Button onClick={handleCreateList} disabled={isCreating || !newListTitle.trim()}> {isCreating ? "Creating..." : "Create List"} </Button> </DialogFooter> </DialogContent> </Dialog>Also applies to: 534-572
app/dashboard/page.tsx (1)
851-876: Per-item delete dialog: same DialogClose fix applies.Replace both inner DialogTrigger components with DialogClose to close reliably after Cancel/Delete.
- <DialogFooter className="gap-2"> - <DialogTrigger asChild> + <DialogFooter className="gap-2"> + <DialogClose asChild> <Button variant="secondary" size="sm"> Cancel </Button> - </DialogTrigger> - <DialogTrigger asChild> + </DialogClose> + <DialogClose asChild> <Button variant="primary" size="sm" className="bg-red-600 hover:bg-red-700 text-white" onClick={() => handleDeleteRule(rule.id)} > Delete </Button> - </DialogTrigger> + </DialogClose>(Import DialogClose as noted above.)
🧹 Nitpick comments (17)
.env.example (1)
1-13: Address dotenv-linter warnings: drop quotes, trim trailing whitespace, add EOF newline.Removes noisy warnings and keeps a clean template.
Apply this diff:
-# Database (production) -DATABASE_URL="postgresql://prod_user:password@host/database" +# Database (production) +DATABASE_URL=postgresql://prod_user:password@host/database -# Email Service -INBOUND_API_KEY="your_production_api_key" +# Email Service +INBOUND_API_KEY=your_production_api_key -# Auth -BETTER_AUTH_SECRET="your_secure_random_string" -BETTER_AUTH_URL="https://your-domain.com" +# Auth +BETTER_AUTH_SECRET=your_secure_random_string +BETTER_AUTH_URL=https://your-domain.com -# GitHub -GITHUB_CLIENT_ID="your-client-id" -GITHUB_CLIENT_SECRET="your-client-secret" +# GitHub +GITHUB_CLIENT_ID=your-client-id +GITHUB_CLIENT_SECRET=your-client-secret +components/dashboard/user-lists.tsx (1)
150-168: Snapshot title before clearing state to ensure correct toast text.Avoid relying on async state timing by storing the title locally.
Apply this diff:
- const response = await fetch('/api/lists', { + const createdTitle = newListTitle.trim() + const response = await fetch('/api/lists', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ title: newListTitle.trim() }) + body: JSON.stringify({ title: createdTitle }) }) ... - setNewListTitle("") - setIsCreateDialogOpen(false) // Close the dialog after successful creation - toast.success(`Created list "${newListTitle}"!`) + setNewListTitle("") + setIsCreateDialogOpen(false) // Close the dialog after successful creation + toast.success(`Created list "${createdTitle}"!`)components/header.tsx (2)
43-50: Add accessible name for icon-only stateOn small screens the label is hidden; add an accessible name so screen readers still announce “Dashboard”.
- <Link href="/dashboard"> + <Link href="/dashboard" aria-label="Dashboard">
46-46: Use React SVG camelCase attributesTSX prefers fillOpacity over fill-opacity to avoid type warnings.
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><title>dashboard</title><g fill="#70A7D7"><path d="M2.75 2.75C2.75 2.336 3.086 2 3.5 2H6.5C6.914 2 7.25 2.336 7.25 2.75V6.5C7.25 6.914 6.914 7.25 6.5 7.25H3.5C3.086 7.25 2.75 6.914 2.75 6.5V2.75Z" fill-opacity="0.4"></path> <path d="M10.75 2.75C10.75 2.336 11.086 2 11.5 2H14.5C14.914 2 15.25 2.336 15.25 2.75V6.5C15.25 6.914 14.914 7.25 14.5 7.25H11.5C11.086 7.25 10.75 6.914 10.75 6.5V2.75Z" fill-opacity="0.4"></path> <path d="M2.75 10.75C2.75 10.336 3.086 10 3.5 10H6.5C6.914 10 7.25 10.336 7.25 10.75V14.5C7.25 14.914 6.914 15.25 6.5 15.25H3.5C3.086 15.25 2.75 14.914 2.75 14.5V10.75Z" fill-opacity="0.4"></path> <path d="M10.75 10.75C10.75 10.336 11.086 10 11.5 10H14.5C14.914 10 15.25 10.336 15.25 10.75V14.5C15.25 14.914 14.914 15.25 14.5 15.25H11.5C11.086 15.25 10.75 14.914 10.75 14.5V10.75Z" fill-opacity="0.4"></path></g></svg> + <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><title>dashboard</title><g fill="#70A7D7"><path d="M2.75 2.75C2.75 2.336 3.086 2 3.5 2H6.5C6.914 2 7.25 2.336 7.25 2.75V6.5C7.25 6.914 6.914 7.25 6.5 7.25H3.5C3.086 7.25 2.75 6.914 2.75 6.5V2.75Z" fillOpacity="0.4"></path> <path d="M10.75 2.75C10.75 2.336 11.086 2 11.5 2H14.5C14.914 2 15.25 2.336 15.25 2.75V6.5C15.25 6.914 14.914 7.25 14.5 7.25H11.5C11.086 7.25 10.75 6.914 10.75 6.5V2.75Z" fillOpacity="0.4"></path> <path d="M2.75 10.75C2.75 10.336 3.086 10 3.5 10H6.5C6.914 10 7.25 10.336 7.25 10.75V14.5C7.25 14.914 6.914 15.25 6.5 15.25H3.5C3.086 15.25 2.75 14.914 2.75 14.5V10.75Z" fillOpacity="0.4"></path> <path d="M10.75 10.75C10.75 10.336 11.086 10 11.5 10H14.5C14.914 10 15.25 10.336 15.25 10.75V14.5C15.25 14.914 14.914 15.25 14.5 15.25H11.5C11.086 15.25 10.75 14.914 10.75 14.5V10.75Z" fillOpacity="0.4"></path></g></svg>app/api/feed/new/route.ts (3)
28-39: Guard against pathological q sizesTrim and cap q length (e.g., 200 chars) to protect against expensive leading-wildcard scans.
The diff above includes
slice(0, 200). Consider documenting this limit in README.
11-27: Optional: extract a shared feed query builderHot/New duplicate selection/join/filters. Factor into a tiny helper (e.g., lib/queries/feed.ts) to keep logic consistent and avoid regressions.
41-44: Indexing advice for search and sortAdd indexes to sustain LIKE searches and sort limits:
- GIN trigram on title, content, and user.name (requires pg_trgm).
- Composite btree on (isPublic, createdAt DESC).
app/api/feed/hot/route.ts (1)
41-44: Consider secondary sort tie-breakerviews DESC then createdAt DESC is good. If ties are frequent, add id DESC to stabilize pagination.
app/feed/page.tsx (2)
231-237: Add accessible label to search inputPlaceholder isn’t an accessible name; add aria-label.
- <Input + <Input type="text" placeholder="Search rules by title, content, or author..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="pl-10 pr-10 bg-[#1B1D21] border-white/10 text-white placeholder:text-gray-400 focus:border-[#70A7D7] focus:ring-[#70A7D7]" + aria-label="Search rules" />
147-159: Minor: cache-busting and analytics consistencyIf results must always be fresh, pass cache: 'no-store'. Also consider tracking when searches execute (debounced value changes) for better analytics.
- fetch(hotUrl, { signal: controller.signal }), - fetch(newUrl, { signal: controller.signal }) + fetch(hotUrl, { signal: controller.signal, cache: 'no-store' }), + fetch(newUrl, { signal: controller.signal, cache: 'no-store' })README.md (1)
206-217: Document that feed search only returns public rulesClarify behavior to match the API intent and avoid user confusion.
-GET /api/feed/hot?q=search_query +GET /api/feed/hot?q=search_query # Searches public rules (title, content, author)-GET /api/feed/new?q=search_query +GET /api/feed/new?q=search_query # Searches public rules (title, content, author)app/dashboard/page.tsx (6)
149-156: Select all/clear look fine.If the list can be very large later, consider virtualized lists before optimizing further.
161-173: Handle non-OK responses from /api/lists.Currently ignores non-2xx. Surface a toast for visibility.
Apply within this block:
const fetchAvailableLists = async () => { try { const response = await fetch('/api/lists') - if (response.ok) { - const lists = await response.json() - setAvailableLists(lists.map((list: any) => ({ id: list.id, title: list.title }))) - } + if (!response.ok) { + toast.error('Failed to fetch lists') + return + } + const lists = await response.json() + setAvailableLists(lists.map((list: any) => ({ id: list.id, title: list.title }))) } catch (error) { console.error('Error fetching lists:', error) } }
413-416: Fetching lists on session ready is fine.Optional: also refresh lists when opening the list dialog (see comment below).
510-528: Minor UX: reflect partial selection.If some (not all) are selected, consider an “indeterminate” checkbox state for better affordance.
660-679: Add basic a11y to selection toggle.Expose checkbox semantics and keyboard toggle.
- <button + <button + role="checkbox" + aria-checked={isRuleSelected(rule.id)} + aria-label={isRuleSelected(rule.id) ? 'Deselect rule' : 'Select rule'} onClick={(e) => { e.stopPropagation() toggleRuleSelection(rule.id) }} className="flex items-center justify-center w-4 h-4 rounded border border-white/20 hover:border-white/40 transition-colors" style={{ backgroundColor: isRuleSelected(rule.id) ? '#70A7D7' : 'transparent' }} title={isRuleSelected(rule.id) ? 'Deselect rule' : 'Select rule'} + onKeyDown={(e) => { + if (e.key === ' ' || e.key === 'Enter') { + e.preventDefault() + toggleRuleSelection(rule.id) + } + }} >
910-959: List dialog is solid; small QoL: refresh lists on open and disable Cancel while adding.Keeps options fresh and avoids accidental close mid-submit.
- <Dialog open={showListDialog} onOpenChange={setShowListDialog}> + <Dialog + open={showListDialog} + onOpenChange={(open) => { + setShowListDialog(open) + if (open) fetchAvailableLists() + }} + > ... - <Button + <Button variant="outline" onClick={() => setShowListDialog(false)} + disabled={isAddingToList} className="border-white/10 bg-transparent hover:bg-white/5" > Cancel </Button>
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (11)
.env.example(1 hunks).gitignore(1 hunks)README.md(5 hunks)app/api/feed/hot/route.ts(2 hunks)app/api/feed/new/route.ts(2 hunks)app/dashboard/page.tsx(6 hunks)app/feed/page.tsx(6 hunks)components/auth/user-avatar.tsx(1 hunks)components/dashboard/user-lists.tsx(4 hunks)components/header.tsx(1 hunks)package.json(1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/better-auth-device-authorization.mdc)
When calling device.token, set grant_type to "urn:ietf:params:oauth:grant-type:device_code"
Files:
components/dashboard/user-lists.tsxcomponents/header.tsxcomponents/auth/user-avatar.tsxapp/api/feed/hot/route.tsapp/dashboard/page.tsxapp/feed/page.tsxapp/api/feed/new/route.ts
🧬 Code graph analysis (4)
app/api/feed/hot/route.ts (2)
app/api/feed/new/route.ts (1)
GET(6-51)lib/schema.ts (2)
cursorRule(49-59)user(3-11)
app/dashboard/page.tsx (1)
lib/schema.ts (1)
list(61-67)
app/feed/page.tsx (1)
components/ui/input.tsx (1)
Input(21-21)
app/api/feed/new/route.ts (3)
app/api/feed/hot/route.ts (1)
GET(6-51)lib/db.ts (1)
db(10-10)lib/schema.ts (2)
cursorRule(49-59)user(3-11)
🪛 dotenv-linter (3.3.0)
.env.example
[warning] 2-2: [QuoteCharacter] The value has quote characters (', ")
(QuoteCharacter)
[warning] 5-5: [QuoteCharacter] The value has quote characters (', ")
(QuoteCharacter)
[warning] 8-8: [QuoteCharacter] The value has quote characters (', ")
(QuoteCharacter)
[warning] 9-9: [QuoteCharacter] The value has quote characters (', ")
(QuoteCharacter)
[warning] 12-12: [TrailingWhitespace] Trailing whitespace detected
(TrailingWhitespace)
[warning] 13-13: [EndingBlankLine] No blank line at the end of the file
(EndingBlankLine)
[warning] 13-13: [QuoteCharacter] The value has quote characters (', ")
(QuoteCharacter)
🪛 LanguageTool
README.md
[grammar] ~16-~16: There might be a mistake here.
Context: ... community-shared rules in Hot/New feeds - 📱 CLI Tool - Full-featured CLI for sy...
(QB_NEW_EN)
[grammar] ~17-~17: There might be a mistake here.
Context: ...rules in Hot/New feeds - 📱 CLI Tool - Full-featured CLI for syncing rules be...
(QB_NEW_EN)
[grammar] ~17-~17: There might be a mistake here.
Context: ...es in Hot/New feeds - 📱 CLI Tool - Full-featured CLI for syncing rules between l...
(QB_NEW_EN)
[grammar] ~17-~17: There might be a mistake here.
Context: ...or syncing rules between local and cloud - 📋 Lists & Collections - Organize rule...
(QB_NEW_EN)
[grammar] ~18-~18: There might be a mistake here.
Context: ...ons** - Organize rules into custom lists - 🎨 Modern UI - Beautiful dark theme wi...
(QB_NEW_EN)
[grammar] ~19-~19: There might be a mistake here.
Context: ...e with Tailwind CSS and Radix components ## 🛠️ Tech Stack ### Frontend - **Next.js...
(QB_NEW_EN)
[grammar] ~21-~21: There might be a mistake here.
Context: ... and Radix components ## 🛠️ Tech Stack ### Frontend - Next.js 15.2.4 - App Rout...
(QB_NEW_EN)
[grammar] ~23-~23: There might be a mistake here.
Context: ...ponents ## 🛠️ Tech Stack ### Frontend - Next.js 15.2.4 - App Router with React...
(QB_NEW_EN)
[grammar] ~24-~24: There might be a mistake here.
Context: ...t.js 15.2.4** - App Router with React 19 - TypeScript 5 - Full type safety - **Ta...
(QB_NEW_EN)
[grammar] ~25-~25: There might be a mistake here.
Context: ...19 - TypeScript 5 - Full type safety - Tailwind CSS 4.1.9 - Modern styling sy...
(QB_NEW_EN)
[grammar] ~26-~26: There might be a mistake here.
Context: ...wind CSS 4.1.9** - Modern styling system - Radix UI - Accessible component primit...
(QB_NEW_EN)
[grammar] ~27-~27: There might be a mistake here.
Context: ...x UI** - Accessible component primitives - React Hook Form + Zod - Form handling ...
(QB_NEW_EN)
[grammar] ~28-~28: There might be a mistake here.
Context: ...m + Zod** - Form handling and validation - Geist Font - Modern typography ### Ba...
(QB_NEW_EN)
[grammar] ~31-~31: There might be a mistake here.
Context: ... Font** - Modern typography ### Backend - PostgreSQL - Primary database (via Neo...
(QB_NEW_EN)
[grammar] ~32-~32: There might be a mistake here.
Context: ...SQL** - Primary database (via Neon.tech) - Drizzle ORM 0.44.5 - Type-safe databas...
(QB_NEW_EN)
[grammar] ~33-~33: There might be a mistake here.
Context: ...RM 0.44.5** - Type-safe database queries - Better Auth 1.3.8-beta.9 - Modern auth...
(QB_NEW_EN)
[grammar] ~34-~34: There might be a mistake here.
Context: ...- Modern authentication with magic links - Inbound Email 4.0.0 - Transactional em...
(QB_NEW_EN)
[grammar] ~37-~37: There might be a mistake here.
Context: ...onal email service ### Tools & Services - Vercel - Deployment and hosting - **Re...
(QB_NEW_EN)
[grammar] ~38-~38: There might be a mistake here.
Context: ...es - Vercel - Deployment and hosting - React Scan 0.4.3 - Performance monitor...
(QB_NEW_EN)
[grammar] ~39-~39: There might be a mistake here.
Context: ...ct Scan 0.4.3** - Performance monitoring - Sonner 2.0.7 - Toast notifications - *...
(QB_NEW_EN)
[grammar] ~40-~40: There might be a mistake here.
Context: ...- Sonner 2.0.7 - Toast notifications - Vercel Analytics - Usage analytics - *...
(QB_NEW_EN)
[grammar] ~41-~41: There might be a mistake here.
Context: ...- Vercel Analytics - Usage analytics - GPT Tokenizer - Token counting for rul...
(QB_NEW_EN)
[grammar] ~248-~248: There might be a mistake here.
Context: ...e-id-1", "rule-id-2"] } ``` ## 🎯 Usage ### Creating Rules 1. *Visit the homepage...
(QB_NEW_EN)
[grammar] ~287-~287: There might be a mistake here.
Context: ...r/rules/` automatically. ## 📱 CLI Tool cursor.link includes a powerful CLI tool...
(QB_NEW_EN)
[grammar] ~339-~339: There might be a mistake here.
Context: ... ID ### How it Works #### Push Process 1. Scans your .cursor/rules/ directory fo...
(QB_NEW_EN)
[grammar] ~340-~340: There might be a mistake here.
Context: ...ursor/rules/directory for.mdc` files 2. Parses each file to extract title, conte...
(QB_NEW_EN)
[grammar] ~341-~341: There might be a mistake here.
Context: ... to extract title, content, and settings 3. Uploads rules to your cursor.link accoun...
(QB_NEW_EN)
[grammar] ~342-~342: There might be a mistake here.
Context: ...ploads rules to your cursor.link account 4. Handles conflicts by asking for your pre...
(QB_NEW_EN)
[grammar] ~345-~345: There might be a mistake here.
Context: ...g for your preference #### Pull Process 1. Fetches available rules from cursor.link...
(QB_NEW_EN)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Vade Review
🔇 Additional comments (10)
package.json (1)
2-2: Rename to “cursor-link” — looks good.No issues spotted with the package rename.
.gitignore (1)
21-21: Good call tracking .env.example.Keeps secrets out while documenting env shape.
components/auth/user-avatar.tsx (1)
185-241: Header includes Dashboard link — resolvedThe top navigation in components/header.tsx (line 44) contains a
<Link href="/dashboard">Dashboard</Link>, so no further changes are needed.components/dashboard/user-lists.tsx (1)
97-99: Alldevice.tokencalls include the required grant_type
Verified the soleauthClient.device.tokeninvocation incursor-link-cli/src/utils/auth-client.tsalready usesgrant_type: "urn:ietf:params:oauth:grant-type:device_code".components/header.tsx (1)
43-50: LGTM: clear, consistent nav affordanceAdding a dedicated Dashboard entry here matches the user menu change and improves discoverability.
README.md (1)
305-310: No action needed—grant_type is correct
Verified in cursor-link-cli/src/utils/auth-client.ts (line 218) that authClient.device.token uses"urn:ietf:params:oauth:grant-type:device_code".app/dashboard/page.tsx (4)
123-127: Good call using Set for multi-select state.Efficient membership checks and clean updates.
136-147: toggleRuleSelection logic is solid.Idempotent, minimal re-renders via copying the Set.
157-160: Helper reads clearly.No changes needed.
606-622: Clear selection control looks good.State and disabled styling are consistent.
app/api/feed/hot/route.ts
Outdated
| // Add search filter if query is provided | ||
| if (searchQuery && searchQuery.trim()) { | ||
| const searchTerm = `%${searchQuery.trim()}%` | ||
| query = query.where( | ||
| or( | ||
| eq(cursorRule.isPublic, true), | ||
| ilike(cursorRule.title, searchTerm), | ||
| ilike(cursorRule.content, searchTerm), | ||
| ilike(user.name, searchTerm) | ||
| ) | ||
| ) | ||
| } |
There was a problem hiding this comment.
Same leakage bug as New feed: OR widens to private content
Apply AND with isPublic as in the New route fix.
-import { eq, desc, or, ilike } from "drizzle-orm"
+import { eq, desc, or, ilike, and } from "drizzle-orm"
@@
- if (searchQuery && searchQuery.trim()) {
- const searchTerm = `%${searchQuery.trim()}%`
- query = query.where(
- or(
- eq(cursorRule.isPublic, true),
- ilike(cursorRule.title, searchTerm),
- ilike(cursorRule.content, searchTerm),
- ilike(user.name, searchTerm)
- )
- )
- }
+ if (searchQuery && searchQuery.trim()) {
+ const searchTerm = `%${searchQuery.trim().slice(0, 200)}%`
+ query = query.where(
+ and(
+ eq(cursorRule.isPublic, true),
+ or(
+ ilike(cursorRule.title, searchTerm),
+ ilike(cursorRule.content, searchTerm),
+ ilike(user.name, searchTerm)
+ )
+ )
+ )
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Add search filter if query is provided | |
| if (searchQuery && searchQuery.trim()) { | |
| const searchTerm = `%${searchQuery.trim()}%` | |
| query = query.where( | |
| or( | |
| eq(cursorRule.isPublic, true), | |
| ilike(cursorRule.title, searchTerm), | |
| ilike(cursorRule.content, searchTerm), | |
| ilike(user.name, searchTerm) | |
| ) | |
| ) | |
| } | |
| // At top of file, include `and` in the import | |
| import { eq, desc, or, ilike, and } from "drizzle-orm" | |
| // ... | |
| // Add search filter if query is provided | |
| if (searchQuery && searchQuery.trim()) { | |
| - const searchTerm = `%${searchQuery.trim()}%` | |
| - query = query.where( | |
| - or( | |
| - eq(cursorRule.isPublic, true), | |
| - ilike(cursorRule.title, searchTerm), | |
| - ilike(cursorRule.content, searchTerm), | |
| - ilike(user.name, searchTerm) | |
| - ) | |
| - ) | |
| // limit search term length to prevent overly long patterns | |
| const searchTerm = `%${searchQuery.trim().slice(0, 200)}%` | |
| query = query.where( | |
| and( | |
| // always require public content… | |
| eq(cursorRule.isPublic, true), | |
| // …and at least one field matches the term | |
| or( | |
| ilike(cursorRule.title, searchTerm), | |
| ilike(cursorRule.content, searchTerm), | |
| ilike(user.name, searchTerm) | |
| ) | |
| ) | |
| ) | |
| } |
🤖 Prompt for AI Agents
In app/api/feed/hot/route.ts around lines 28 to 39, the search filter currently
wraps isPublic inside an OR which expands results to include private content
when the search matches; replace that logic so isPublic is always required and
the search terms are applied as an additional condition: wrap the ilike clauses
in an or(...) and combine that or(...) with eq(cursorRule.isPublic, true) using
and(...), i.e. change the where(...) call to require isPublic true AND (title
ilike OR content ilike OR user.name ilike) so private items are not returned by
searches.
app/api/feed/new/route.ts
Outdated
| // Add search filter if query is provided | ||
| if (searchQuery && searchQuery.trim()) { | ||
| const searchTerm = `%${searchQuery.trim()}%` | ||
| query = query.where( | ||
| or( | ||
| eq(cursorRule.isPublic, true), | ||
| ilike(cursorRule.title, searchTerm), | ||
| ilike(cursorRule.content, searchTerm), | ||
| ilike(user.name, searchTerm) | ||
| ) | ||
| ) | ||
| } |
There was a problem hiding this comment.
Private rules can leak via OR search; combine with isPublic using AND
The second where() overrides the first and the OR condition allows matches on non-public rows. Must AND the search group with isPublic.
-import { eq, desc, or, ilike } from "drizzle-orm"
+import { eq, desc, or, ilike, and } from "drizzle-orm"
@@
- // Add search filter if query is provided
- if (searchQuery && searchQuery.trim()) {
- const searchTerm = `%${searchQuery.trim()}%`
- query = query.where(
- or(
- eq(cursorRule.isPublic, true),
- ilike(cursorRule.title, searchTerm),
- ilike(cursorRule.content, searchTerm),
- ilike(user.name, searchTerm)
- )
- )
- }
+ // Add search filter if query is provided
+ if (searchQuery && searchQuery.trim()) {
+ const searchTerm = `%${searchQuery.trim().slice(0, 200)}%`
+ // Preserve isPublic = true AND apply (title OR content OR author) filter
+ query = query.where(
+ and(
+ eq(cursorRule.isPublic, true),
+ or(
+ ilike(cursorRule.title, searchTerm),
+ ilike(cursorRule.content, searchTerm),
+ ilike(user.name, searchTerm)
+ )
+ )
+ )
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Add search filter if query is provided | |
| if (searchQuery && searchQuery.trim()) { | |
| const searchTerm = `%${searchQuery.trim()}%` | |
| query = query.where( | |
| or( | |
| eq(cursorRule.isPublic, true), | |
| ilike(cursorRule.title, searchTerm), | |
| ilike(cursorRule.content, searchTerm), | |
| ilike(user.name, searchTerm) | |
| ) | |
| ) | |
| } | |
| // At the top of the file, include `and` alongside the other imports | |
| import { eq, desc, or, ilike, and } from "drizzle-orm" | |
| // … | |
| // Add search filter if query is provided | |
| if (searchQuery && searchQuery.trim()) { | |
| - const searchTerm = `%${searchQuery.trim()}%` | |
| - query = query.where( | |
| - or( | |
| - eq(cursorRule.isPublic, true), | |
| - ilike(cursorRule.title, searchTerm), | |
| - ilike(cursorRule.content, searchTerm), | |
| - ilike(user.name, searchTerm) | |
| - ) | |
| // Limit the length of the search term to avoid overly long queries | |
| const searchTerm = `%${searchQuery.trim().slice(0, 200)}%` | |
| // Ensure only public rules are returned, then apply the search filters | |
| query = query.where( | |
| and( | |
| eq(cursorRule.isPublic, true), | |
| or( | |
| ilike(cursorRule.title, searchTerm), | |
| ilike(cursorRule.content, searchTerm), | |
| ilike(user.name, searchTerm) | |
| ) | |
| ) | |
| ) | |
| } |
🤖 Prompt for AI Agents
In app/api/feed/new/route.ts around lines 28 to 39, the second query.where(...)
uses an OR that allows non-public rows to match and effectively overrides the
previous isPublic filter; change the query to ensure the search group is
combined with isPublic using AND (for example, wrap the existing OR inside an
AND with eq(cursorRule.isPublic, true) or use the ORM's andWhere/merge
mechanism) so that search matches must also have isPublic=true.
| // Handle adding selected rules to a list | ||
| const handleAddSelectedToList = async (listId: string) => { | ||
| if (selectedRules.size === 0) return | ||
|
|
||
| setIsAddingToList(true) | ||
| try { | ||
| const promises = Array.from(selectedRules).map(ruleId => | ||
| fetch(`/api/lists/${listId}/rules`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ ruleId }) | ||
| }) | ||
| ) | ||
|
|
||
| const results = await Promise.allSettled(promises) | ||
| const successful = results.filter(result => result.status === 'fulfilled').length | ||
| const failed = results.length - successful | ||
|
|
||
| if (successful > 0) { | ||
| toast.success(`Added ${successful} rule${successful !== 1 ? 's' : ''} to list!`) | ||
| track("Rules Added to List", { count: successful, listId }) | ||
| } | ||
| if (failed > 0) { | ||
| toast.error(`Failed to add ${failed} rule${failed !== 1 ? 's' : ''} (may already be in list)`) | ||
| } | ||
|
|
||
| setShowListDialog(false) | ||
| clearSelection() | ||
| } catch (error) { | ||
| console.error('Error adding rules to list:', error) | ||
| toast.error('Failed to add rules to list') | ||
| } finally { | ||
| setIsAddingToList(false) | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Count success by Response.ok, not just fulfilled promises; snapshot selection.
4xx/5xx resolve the promise but should be treated as failures. Also snapshot selected IDs to avoid races.
const handleAddSelectedToList = async (listId: string) => {
- if (selectedRules.size === 0) return
+ const ids = Array.from(selectedRules)
+ if (ids.length === 0) return
setIsAddingToList(true)
try {
- const promises = Array.from(selectedRules).map(ruleId =>
- fetch(`/api/lists/${listId}/rules`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ ruleId })
- })
- )
-
- const results = await Promise.allSettled(promises)
- const successful = results.filter(result => result.status === 'fulfilled').length
- const failed = results.length - successful
+ const requests = ids.map(ruleId =>
+ fetch(`/api/lists/${listId}/rules`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ ruleId })
+ })
+ )
+ const settled = await Promise.allSettled(requests)
+ let successful = 0
+ for (const r of settled) {
+ if (r.status === 'fulfilled' && r.value.ok) successful++
+ }
+ const failed = ids.length - successful
if (successful > 0) {
toast.success(`Added ${successful} rule${successful !== 1 ? 's' : ''} to list!`)
track("Rules Added to List", { count: successful, listId })
}
if (failed > 0) {
- toast.error(`Failed to add ${failed} rule${failed !== 1 ? 's' : ''} (may already be in list)`)
+ toast.error(`Failed to add ${failed} rule${failed !== 1 ? 's' : ''} (already in list or server error)`)
}
setShowListDialog(false)
clearSelection()
} catch (error) {
console.error('Error adding rules to list:', error)
toast.error('Failed to add rules to list')
} finally {
setIsAddingToList(false)
}
}If this becomes a hot path, consider a batch API (POST /api/lists/{id}/rules: { ruleIds: string[] }) to reduce N requests.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Handle adding selected rules to a list | |
| const handleAddSelectedToList = async (listId: string) => { | |
| if (selectedRules.size === 0) return | |
| setIsAddingToList(true) | |
| try { | |
| const promises = Array.from(selectedRules).map(ruleId => | |
| fetch(`/api/lists/${listId}/rules`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ ruleId }) | |
| }) | |
| ) | |
| const results = await Promise.allSettled(promises) | |
| const successful = results.filter(result => result.status === 'fulfilled').length | |
| const failed = results.length - successful | |
| if (successful > 0) { | |
| toast.success(`Added ${successful} rule${successful !== 1 ? 's' : ''} to list!`) | |
| track("Rules Added to List", { count: successful, listId }) | |
| } | |
| if (failed > 0) { | |
| toast.error(`Failed to add ${failed} rule${failed !== 1 ? 's' : ''} (may already be in list)`) | |
| } | |
| setShowListDialog(false) | |
| clearSelection() | |
| } catch (error) { | |
| console.error('Error adding rules to list:', error) | |
| toast.error('Failed to add rules to list') | |
| } finally { | |
| setIsAddingToList(false) | |
| } | |
| } | |
| // Handle adding selected rules to a list | |
| const handleAddSelectedToList = async (listId: string) => { | |
| const ids = Array.from(selectedRules) | |
| if (ids.length === 0) return | |
| setIsAddingToList(true) | |
| try { | |
| const requests = ids.map(ruleId => | |
| fetch(`/api/lists/${listId}/rules`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ ruleId }) | |
| }) | |
| ) | |
| const settled = await Promise.allSettled(requests) | |
| let successful = 0 | |
| for (const r of settled) { | |
| if (r.status === 'fulfilled' && r.value.ok) successful++ | |
| } | |
| const failed = ids.length - successful | |
| if (successful > 0) { | |
| toast.success(`Added ${successful} rule${successful !== 1 ? 's' : ''} to list!`) | |
| track("Rules Added to List", { count: successful, listId }) | |
| } | |
| if (failed > 0) { | |
| toast.error(`Failed to add ${failed} rule${failed !== 1 ? 's' : ''} (already in list or server error)`) | |
| } | |
| setShowListDialog(false) | |
| clearSelection() | |
| } catch (error) { | |
| console.error('Error adding rules to list:', error) | |
| toast.error('Failed to add rules to list') | |
| } finally { | |
| setIsAddingToList(false) | |
| } | |
| } |
🤖 Prompt for AI Agents
In app/dashboard/page.tsx around lines 174 to 208, snapshot the current selected
rule IDs into a local array before any async work to avoid races, then perform
the fetches and treat a request as successful only if the Response.ok is true
(counting non-ok 4xx/5xx as failures). Concretely: copy selectedRules to an
array up-front, map that array to async fetch calls that check response.ok (and
throw or return a failure marker when not ok), await Promise.all or
Promise.allSettled and compute successful based on response.ok (or resolved
value) rather than settled status, then proceed to show success/error toasts and
clear selection. Ensure setIsAddingToList is still toggled in finally.
| // Handle deleting selected rules | ||
| const handleDeleteSelected = async () => { | ||
| if (selectedRules.size === 0) return | ||
|
|
||
| try { | ||
| const promises = Array.from(selectedRules).map(ruleId => | ||
| fetch('/api/cursor-rules', { | ||
| method: 'DELETE', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ id: ruleId }) | ||
| }) | ||
| ) | ||
|
|
||
| const results = await Promise.allSettled(promises) | ||
| const successful = results.filter(result => result.status === 'fulfilled').length | ||
|
|
||
| if (successful > 0) { | ||
| // Remove successfully deleted rules from local state | ||
| setRules(prevRules => prevRules.filter(rule => !selectedRules.has(rule.id))) | ||
| toast.success(`Deleted ${successful} rule${successful !== 1 ? 's' : ''}!`) | ||
| track("Rules Deleted", { count: successful }) | ||
| clearSelection() | ||
| } | ||
| } catch (error) { | ||
| console.error('Error deleting rules:', error) | ||
| toast.error('Failed to delete rules') | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Delete selected: verify Response.ok, remove only actually-deleted IDs, snapshot selection.
Current logic may report success when server returns 4xx/5xx and may race with selection changes.
const handleDeleteSelected = async () => {
- if (selectedRules.size === 0) return
+ const ids = Array.from(selectedRules)
+ if (ids.length === 0) return
try {
- const promises = Array.from(selectedRules).map(ruleId =>
- fetch('/api/cursor-rules', {
- method: 'DELETE',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ id: ruleId })
- })
- )
-
- const results = await Promise.allSettled(promises)
- const successful = results.filter(result => result.status === 'fulfilled').length
+ const results = await Promise.all(
+ ids.map(ruleId =>
+ fetch('/api/cursor-rules', {
+ method: 'DELETE',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ id: ruleId })
+ })
+ .then(res => ({ id: ruleId, ok: res.ok }))
+ .catch(() => ({ id: ruleId, ok: false }))
+ )
+ )
+ const successfulIds = results.filter(r => r.ok).map(r => r.id)
+ const successful = successfulIds.length
+ const failed = ids.length - successful
if (successful > 0) {
- // Remove successfully deleted rules from local state
- setRules(prevRules => prevRules.filter(rule => !selectedRules.has(rule.id)))
+ // Remove successfully deleted rules from local state
+ setRules(prevRules => prevRules.filter(rule => !successfulIds.includes(rule.id)))
toast.success(`Deleted ${successful} rule${successful !== 1 ? 's' : ''}!`)
track("Rules Deleted", { count: successful })
clearSelection()
}
+ if (failed > 0) {
+ toast.error(`Failed to delete ${failed} rule${failed !== 1 ? 's' : ''}`)
+ }
} catch (error) {
console.error('Error deleting rules:', error)
toast.error('Failed to delete rules')
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Handle deleting selected rules | |
| const handleDeleteSelected = async () => { | |
| if (selectedRules.size === 0) return | |
| try { | |
| const promises = Array.from(selectedRules).map(ruleId => | |
| fetch('/api/cursor-rules', { | |
| method: 'DELETE', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ id: ruleId }) | |
| }) | |
| ) | |
| const results = await Promise.allSettled(promises) | |
| const successful = results.filter(result => result.status === 'fulfilled').length | |
| if (successful > 0) { | |
| // Remove successfully deleted rules from local state | |
| setRules(prevRules => prevRules.filter(rule => !selectedRules.has(rule.id))) | |
| toast.success(`Deleted ${successful} rule${successful !== 1 ? 's' : ''}!`) | |
| track("Rules Deleted", { count: successful }) | |
| clearSelection() | |
| } | |
| } catch (error) { | |
| console.error('Error deleting rules:', error) | |
| toast.error('Failed to delete rules') | |
| } | |
| } | |
| // Handle deleting selected rules | |
| const handleDeleteSelected = async () => { | |
| // Snapshot the current selection | |
| const ids = Array.from(selectedRules) | |
| if (ids.length === 0) return | |
| try { | |
| // Perform all deletes and check each response.ok | |
| const results = await Promise.all( | |
| ids.map(ruleId => | |
| fetch('/api/cursor-rules', { | |
| method: 'DELETE', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ id: ruleId }) | |
| }) | |
| .then(res => ({ id: ruleId, ok: res.ok })) | |
| .catch(() => ({ id: ruleId, ok: false })) | |
| ) | |
| ) | |
| const successfulIds = results.filter(r => r.ok).map(r => r.id) | |
| const successful = successfulIds.length | |
| const failed = ids.length - successful | |
| if (successful > 0) { | |
| // Remove only actually-deleted rules from local state | |
| setRules(prevRules => prevRules.filter(rule => !successfulIds.includes(rule.id))) | |
| toast.success(`Deleted ${successful} rule${successful !== 1 ? 's' : ''}!`) | |
| track("Rules Deleted", { count: successful }) | |
| clearSelection() | |
| } | |
| if (failed > 0) { | |
| toast.error(`Failed to delete ${failed} rule${failed !== 1 ? 's' : ''}`) | |
| } | |
| } catch (error) { | |
| console.error('Error deleting rules:', error) | |
| toast.error('Failed to delete rules') | |
| } | |
| } |
🤖 Prompt for AI Agents
In app/dashboard/page.tsx around lines 210 to 237, snapshot the current
selection at the start (e.g., const ids = Array.from(selectedRules)) to avoid
races, then perform the DELETE requests for that snapshot; after awaiting
results, for each fulfilled promise verify response.ok (and optionally parse
JSON) and only treat those with ok as actually deleted, collect their IDs,
remove only those IDs from local rules state and from selection (do not assume
all selected were deleted), and update the toast/track counts using the number
of actually-deleted IDs; preserve the try/catch but change success logic to rely
on response.ok rather than promise fulfillment alone.
| <Dialog> | ||
| <DialogTrigger asChild> | ||
| <button | ||
| disabled={selectedRules.size === 0} | ||
| className={`flex items-center gap-1 px-1.5 py-0.5 rounded-md transition-colors text-xs group ${ | ||
| selectedRules.size > 0 | ||
| ? 'hover:bg-red-500/10 text-red-500' | ||
| : 'text-gray-600 cursor-not-allowed opacity-50' | ||
| }`} | ||
| title={selectedRules.size > 0 ? "Delete selected rules" : "Select rules to delete"} | ||
| > | ||
| <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 18 18"> | ||
| <title>trash-2</title> | ||
| <g fill={selectedRules.size > 0 ? "#EF4444" : "#6B7280"} className={selectedRules.size > 0 ? "group-hover:fill-red-400" : ""}> | ||
| <path opacity="0.4" d="M3.40771 5L3.90253 14.3892C3.97873 15.8531 5.18472 17 6.64862 17H11.3527C12.8166 17 14.0226 15.853 14.0988 14.3896L14.5936 5H3.40771Z"></path> | ||
| <path d="M7.37407 14.0001C6.98007 14.0001 6.64908 13.69 6.62608 13.2901L6.37608 8.7901C6.35408 8.3801 6.67008 8.02006 7.08308 8.00006C7.48908 7.98006 7.85107 8.29002 7.87407 8.71002L8.12407 13.21C8.14707 13.62 7.83007 13.9801 7.41707 14.0001H7.37407Z"></path> | ||
| <path d="M10.6261 14.0001H10.5831C10.1701 13.9801 9.85408 13.62 9.87608 13.21L10.1261 8.71002C10.1491 8.29012 10.4981 7.98006 10.9171 8.00006C11.3301 8.02006 11.6471 8.3801 11.6241 8.7901L11.3741 13.2901C11.3521 13.69 11.0211 14.0001 10.6261 14.0001Z"></path> | ||
| <path d="M15.25 4H12V2.75C12 1.7852 11.2148 1 10.25 1H7.75C6.7852 1 6 1.7852 6 2.75V4H2.75C2.3359 4 2 4.3359 2 4.75C2 5.1641 2.3359 5.5 2.75 5.5H15.25C15.6641 5.5 16 5.1641 16 4.75C16 4.3359 15.6641 4 15.25 4ZM7.5 2.75C7.5 2.6143 7.6143 2.5 7.75 2.5H10.25C10.3857 2.5 10.5 2.6143 10.5 2.75V4H7.5V2.75Z"></path> | ||
| </g> | ||
| </svg> | ||
| Delete | ||
| </button> | ||
| </DialogTrigger> | ||
| <DialogContent className="bg-[#1B1D21] border-white/10 text-white"> | ||
| <DialogHeader> | ||
| <DialogTitle>Delete Selected Rules</DialogTitle> | ||
| <DialogDescription className="text-gray-400"> | ||
| Are you sure you want to delete {selectedRules.size} rule{selectedRules.size !== 1 ? 's' : ''}? This action cannot be undone. | ||
| </DialogDescription> | ||
| </DialogHeader> | ||
| <DialogFooter className="gap-2"> | ||
| <DialogTrigger asChild> | ||
| <Button variant="secondary" size="sm"> | ||
| Cancel | ||
| </Button> | ||
| </DialogTrigger> | ||
| <DialogTrigger asChild> | ||
| <Button | ||
| variant="primary" | ||
| size="sm" | ||
| className="bg-red-600 hover:bg-red-700 text-white" | ||
| onClick={handleDeleteSelected} | ||
| > | ||
| Delete {selectedRules.size} Rule{selectedRules.size !== 1 ? 's' : ''} | ||
| </Button> | ||
| </DialogTrigger> | ||
| </DialogFooter> | ||
| </DialogContent> | ||
| </Dialog> |
There was a problem hiding this comment.
Dialog closing is broken: use DialogClose (not DialogTrigger) inside the dialog.
Cancel/Delete buttons won’t close reliably; DialogTrigger is for opening. Replace with DialogClose.
- <DialogFooter className="gap-2">
- <DialogTrigger asChild>
+ <DialogFooter className="gap-2">
+ <DialogClose asChild>
<Button variant="secondary" size="sm">
Cancel
</Button>
- </DialogTrigger>
- <DialogTrigger asChild>
+ </DialogClose>
+ <DialogClose asChild>
<Button
variant="primary"
size="sm"
className="bg-red-600 hover:bg-red-700 text-white"
onClick={handleDeleteSelected}
>
Delete {selectedRules.size} Rule{selectedRules.size !== 1 ? 's' : ''}
</Button>
- </DialogTrigger>
+ </DialogClose>Add import (outside this hunk):
// at the dialog import line
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, DialogClose } from "@/components/ui/dialog"🤖 Prompt for AI Agents
In app/dashboard/page.tsx around lines 556 to 604, the dialog uses DialogTrigger
for the Cancel and Delete buttons inside the dialog which is incorrect; replace
those inner DialogTrigger usages with DialogClose (using asChild where
appropriate) so the buttons close the dialog reliably, keep the Delete button's
onClick handler intact, and add DialogClose to the dialog imports at the top
(import alongside the other Dialog components).
| "use client" | ||
|
|
||
| import { useEffect, useState } from "react" | ||
| import { useEffect, useState, useCallback } from "react" |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Avoid stale updates: add AbortController to debounced fetches
Without cancellation, slower prior requests can overwrite newer results. Use a shared AbortController.
-import { useEffect, useState, useCallback } from "react"
+import { useEffect, useState, useCallback, useRef } from "react" const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('')
+ const abortRef = useRef<AbortController | null>(null) useEffect(() => {
const fetchRules = async () => {
- setLoading(true)
+ setLoading(true)
+ // cancel previous in-flight fetches
+ abortRef.current?.abort()
+ const controller = new AbortController()
+ abortRef.current = controller
try {
@@
- const [hotResponse, newResponse] = await Promise.all([
- fetch(hotUrl),
- fetch(newUrl)
- ])
+ const [hotResponse, newResponse] = await Promise.all([
+ fetch(hotUrl, { signal: controller.signal }),
+ fetch(newUrl, { signal: controller.signal })
+ ])
@@
}
fetchRules()
- }, [debouncedSearchQuery])
+ return () => abortRef.current?.abort()
+ }, [debouncedSearchQuery])📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { useEffect, useState, useCallback } from "react" | |
| import { useEffect, useState, useCallback, useRef } from "react" | |
| function FeedPage() { | |
| const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('') | |
| const abortRef = useRef<AbortController | null>(null) | |
| useEffect(() => { | |
| const fetchRules = async () => { | |
| setLoading(true) | |
| // cancel any previous in-flight fetches | |
| abortRef.current?.abort() | |
| const controller = new AbortController() | |
| abortRef.current = controller | |
| try { | |
| const [hotResponse, newResponse] = await Promise.all([ | |
| fetch(hotUrl, { signal: controller.signal }), | |
| fetch(newUrl, { signal: controller.signal }) | |
| ]) | |
| // ...process responses... | |
| } catch (error) { | |
| if ((error as Error).name === 'AbortError') { | |
| // request was cancelled; safe to ignore | |
| } else { | |
| // handle other errors | |
| } | |
| } finally { | |
| setLoading(false) | |
| } | |
| } | |
| fetchRules() | |
| return () => { | |
| // cleanup on unmount or before next effect run | |
| abortRef.current?.abort() | |
| } | |
| }, [debouncedSearchQuery]) | |
| // ...rest of component... | |
| } |
🤖 Prompt for AI Agents
In app/feed/page.tsx around line 3, the debounced fetches can cause stale
responses to overwrite newer results; introduce a shared AbortController so each
new debounced request aborts the previous one and pass its signal into fetch
calls, ensure you call controller.abort() before starting a new request, catch
and ignore abort errors (or check error.name === 'AbortError'), and clean up by
aborting the controller in the component unmount/useEffect cleanup to prevent
memory leaks.
- Consolidated search logic into a single where condition for improved readability and maintainability. - Enhanced search capabilities to filter rules by title, content, or author while ensuring only public rules are returned. - Removed redundant code for search filtering, streamlining the query construction process.
Screen.Recording.2025-09-02.at.7.08.41.PM.mov |
|
okay @csark0812 this is pretty nice 😁 |
|
lol glad you think so |
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
app/api/feed/hot/route.ts (1)
4-4: Public filter fix looks good; resolves prior leakage.Importing and() and using it below properly constrains searches to isPublic = true. This addresses the earlier OR-based exposure flagged in prior reviews.
🧹 Nitpick comments (3)
app/api/feed/hot/route.ts (3)
11-26: Type the where condition, drop the non-null assertion, and harden the search term.
- Remove the unnecessary non-null assertion (!) on and(...).
- Type whereCondition to the Drizzle SQL type to satisfy TS.
- Cap input length and escape LIKE wildcards (% and _) to avoid pathological scans and ensure literal substring semantics.
Apply:
- // Build where condition - let whereCondition + // Build where condition + let whereCondition: SQL<unknown> @@ - if (searchQuery && searchQuery.trim()) { - const searchTerm = `%${searchQuery.trim()}%` - whereCondition = and( + if (searchQuery && searchQuery.trim()) { + const raw = searchQuery.trim().slice(0, 200) + const escaped = raw.replace(/[%_\\]/g, "\\$&") + const searchTerm = `%${escaped}%` + whereCondition = and( eq(cursorRule.isPublic, true), or( ilike(cursorRule.title, searchTerm), ilike(cursorRule.content, searchTerm), ilike(user.name, searchTerm) ) - )! + ) } else { whereCondition = eq(cursorRule.isPublic, true) }Add the type import near the top of the file:
import { eq, desc, or, ilike, and } from "drizzle-orm" +import type { SQL } from "drizzle-orm"
28-28: Prefer const for the query builder variable.It’s not reassigned.
- let query = db + const query = db
45-48: Add supporting indexes for common paths (no-search and search).
- No-search path: WHERE isPublic = true ORDER BY views DESC, createdAt DESC LIMIT 50 benefits from a composite index.
- Search path with ILIKE will scan; consider pg_trgm GIN indexes to keep it responsive.
Example PostgreSQL migrations:
-- For hot (no-search) CREATE INDEX IF NOT EXISTS idx_cursor_rule_public_views_createdat ON cursor_rule ( "isPublic", "views" DESC, "createdAt" DESC ); -- Enable trigram and add search indexes CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE INDEX IF NOT EXISTS idx_cursor_rule_title_trgm ON cursor_rule USING gin ("title" gin_trgm_ops); CREATE INDEX IF NOT EXISTS idx_cursor_rule_content_trgm ON cursor_rule USING gin ("content" gin_trgm_ops); CREATE INDEX IF NOT EXISTS idx_user_name_trgm ON "user" USING gin ("name" gin_trgm_ops);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
app/api/feed/hot/route.ts(2 hunks)app/api/feed/new/route.ts(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- app/api/feed/new/route.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/better-auth-device-authorization.mdc)
When calling device.token, set grant_type to "urn:ietf:params:oauth:grant-type:device_code"
Files:
app/api/feed/hot/route.ts
🧬 Code graph analysis (1)
app/api/feed/hot/route.ts (3)
app/api/feed/new/route.ts (1)
GET(6-55)lib/schema.ts (2)
cursorRule(49-59)user(3-11)lib/db.ts (1)
db(10-10)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Vade Review
🔇 Additional comments (1)
app/api/feed/hot/route.ts (1)
8-10: Query param handling is fine.Correctly derives q from the request URL.
| const results = await Promise.allSettled(promises) | ||
| const successful = results.filter(result => result.status === 'fulfilled').length |
There was a problem hiding this comment.
The batch delete and add-to-list operations incorrectly count successful HTTP requests by checking Promise fulfillment status instead of actual HTTP response status codes.
View Details
📝 Patch Details
diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx
index 971c6db..e9b7d76 100644
--- a/app/dashboard/page.tsx
+++ b/app/dashboard/page.tsx
@@ -186,7 +186,9 @@ export default function DashboardPage() {
)
const results = await Promise.allSettled(promises)
- const successful = results.filter(result => result.status === 'fulfilled').length
+ const successful = results.filter(result =>
+ result.status === 'fulfilled' && result.value.ok
+ ).length
const failed = results.length - successful
if (successful > 0) {
@@ -221,15 +223,29 @@ export default function DashboardPage() {
)
const results = await Promise.allSettled(promises)
- const successful = results.filter(result => result.status === 'fulfilled').length
+ const successful = results.filter(result =>
+ result.status === 'fulfilled' && result.value.ok
+ ).length
+ const failed = selectedRules.size - successful
if (successful > 0) {
// Remove successfully deleted rules from local state
- setRules(prevRules => prevRules.filter(rule => !selectedRules.has(rule.id)))
+ // Only remove rules that were actually deleted (successful requests)
+ const successfulRuleIds = new Set()
+ results.forEach((result, index) => {
+ if (result.status === 'fulfilled' && result.value.ok) {
+ const ruleId = Array.from(selectedRules)[index]
+ successfulRuleIds.add(ruleId)
+ }
+ })
+ setRules(prevRules => prevRules.filter(rule => !successfulRuleIds.has(rule.id)))
toast.success(`Deleted ${successful} rule${successful !== 1 ? 's' : ''}!`)
track("Rules Deleted", { count: successful })
clearSelection()
}
+ if (failed > 0) {
+ toast.error(`Failed to delete ${failed} rule${failed !== 1 ? 's' : ''}`)
+ }
} catch (error) {
console.error('Error deleting rules:', error)
toast.error('Failed to delete rules')
Analysis
HTTP Error Response Counting Bug in Batch Operations
Bug Summary
The batch delete and add-to-list operations in the dashboard incorrectly count HTTP error responses (404, 500, etc.) as successful operations due to improper use of Promise.allSettled() with the fetch API.
How the Bug Manifests
When users perform batch operations (deleting multiple rules or adding multiple rules to lists), the system shows misleading success messages and incorrectly updates local state even when some or all HTTP requests fail with error status codes.
Example scenario:
- User selects 5 rules to delete
- 2 deletions return HTTP 200 (success)
- 3 deletions return HTTP 404 (not found)
- Current buggy behavior: Shows "Deleted 5 rules!" and removes all 5 from UI
- Correct behavior: Should show "Deleted 2 rules!" and "Failed to delete 3 rules"
Root Cause Analysis
The fetch API has a specific behavior that differs from other promise-based APIs: fetch only rejects on network errors, not HTTP error status codes. According to the MDN documentation, "A fetch() promise does not reject if the server responds with HTTP status codes that indicate errors (404, 504, etc.). Instead, a then() handler must check the Response.ok and/or Response.status properties."
The buggy code uses this pattern:
const results = await Promise.allSettled(promises)
const successful = results.filter(result => result.status === 'fulfilled').lengthThis counts all resolved fetch promises as successful, including those that returned HTTP error codes like 404 or 500.
Impact Assessment
User Experience Impact:
- Users see false success messages
- UI state becomes inconsistent with server state (showing deleted items that weren't actually deleted)
- Users may not realize operations failed and won't retry
- Leads to confusion and potential data integrity issues
Technical Impact:
- Local state diverges from server state
- No proper error handling or retry mechanisms for failed requests
- Potential for silent failures in critical operations
Technical Validation
Testing confirmed the bug exists in two locations:
handleAddSelectedToList(line 188-189): Batch adding rules to listshandleDeleteSelected(line 223-224): Batch deleting rules
A simulation with mixed HTTP status codes (200, 404, 500, 201) showed the current code reports 4/4 successes while the correct count is 2/4 successes - a 50% overcount rate.
Solution Implemented
The fix checks both promise fulfillment AND HTTP response status:
const successful = results.filter(result =>
result.status === 'fulfilled' && result.value.ok
).lengthAdditionally, for delete operations, the fix ensures only actually deleted items are removed from local state by tracking which specific requests succeeded.
Additional Resources:
- Fetch API Documentation
- Promise.allSettled() Reference
No newline at end of file
🚀 Enhanced Feed & Dashboard Functionality with Project Rebranding
📋 Summary
This PR introduces significant improvements to the cursor.link platform, including enhanced feed functionality, improved dashboard capabilities, and a complete project rebranding. The changes focus on improving user experience through better search capabilities, rule management features, and updated documentation.
✨ Key Features Added
🔍 Enhanced Feed Functionality
🎯 Improved Dashboard Experience
🎯 Navigation Improvements
📚 Project Rebranding & Documentation
🔧 Development Environment
.env.examplefile for easier development setup.gitignorefor better development workflow📁 Files Changed
app/api/feed/hot/route.tsapp/api/feed/new/route.tsapp/dashboard/page.tsxapp/feed/page.tsxcomponents/header.tsxcomponents/auth/user-avatar.tsxcomponents/dashboard/user-lists.tsxREADME.mdpackage.json.env.example.gitignore🎯 Impact
📝 Commit History
update env- Added environment configurationadd dashboard to top buttons- Navigation improvementsEnhance feed and dashboard functionality with search and selection features- Core functionality enhancementsRename project to cursor-link and update README for new features and dependencies- Project rebranding and documentationSummary by CodeRabbit
New Features
Improvements
Documentation
Chores