-
Notifications
You must be signed in to change notification settings - Fork 51
Daily branch 2025 08 27 #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Caution Review failedThe pull request is closed. WalkthroughIntroduces paginated chats and messages with infinite scroll, per-chat deletion, sidebar restructuring, and a new chat initialization/switching flow in global state. Adds Pro entitlement checks, WorkOS+Stripe upgrade and billing flows, per-plan rate limiting, related API routes, and UI updates (headers, user nav, loaders). Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant U as User
participant MS as Messages (UI)
participant GS as GlobalState
participant CQ as Convex<br/>messages.getMessagesByChatId
U->>MS: Scroll near top
MS->>GS: Check isSwitchingChats & paginationStatus
alt CanLoadMore and not switching
MS->>CQ: loadMore(28) with paginationOpts
CQ-->>MS: page (newest-first)
MS->>MS: prepend messages, show top loader
else Exhausted or switching
MS-->>U: No load
end
sequenceDiagram
autonumber
participant U as User
participant CI as ChatItem
participant CH as convex.chats.deleteChat
participant GS as GlobalState
participant R as Router
U->>CI: Click ellipsis → Delete
CI->>CH: mutation(chatId)
CH-->>CI: success
alt Deleted active chat
CI->>GS: initializeNewChat()
CI->>R: navigate("/")
else Deleted inactive chat
CI-->>U: stay in place
end
sequenceDiagram
autonumber
participant U as User
participant H as ChatHeader
participant HU as useUpgrade
participant SUB as /api/subscribe
participant WO as WorkOS
participant ST as Stripe
U->>H: Click "Upgrade to Pro"
H->>HU: handleUpgrade()
HU->>SUB: POST
SUB->>WO: Find/Create org, membership
SUB->>ST: Find/Create customer, create Checkout session
ST-->>SUB: session.url
SUB-->>HU: { url }
HU->>U: router.push(url)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. 📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (8)
✨ Finishing Touches
🧪 Generate unit tests
🪧 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 (
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
convex/chats.ts (4)
37-81: getChatById lacks ownership checks; expose only the caller’s chat.Currently allows reading any chat by id. Either make it service-key only, or switch to user-auth and verify ownership. Since the client calls this directly, prefer user-auth.
Apply:
-export const getChatById = query({ - args: { serviceKey: v.optional(v.string()), id: v.string() }, +export const getChatById = query({ + args: { id: v.string() }, returns: v.union( v.object({ _id: v.id("chats"), _creationTime: v.number(), id: v.string(), title: v.string(), user_id: v.string(), finish_reason: v.optional(v.string()), todos: v.optional( v.array( v.object({ id: v.string(), content: v.string(), status: v.union( v.literal("pending"), v.literal("in_progress"), v.literal("completed"), v.literal("cancelled"), ), }), ), ), update_time: v.number(), }), v.null(), ), handler: async (ctx, args) => { - // Verify service role key - validateServiceKey(args.serviceKey); - - try { - const chat = await ctx.db - .query("chats") - .withIndex("by_chat_id", (q) => q.eq("id", args.id)) - .first(); - return chat || null; + const identity = await ctx.auth.getUserIdentity(); + if (!identity) { + throw new Error("Unauthorized"); + } + try { + const chat = await ctx.db + .query("chats") + .withIndex("by_chat_id", (q) => q.eq("id", args.id)) + .first(); + if (!chat) return null; + if (chat.user_id !== identity.subject) { + throw new Error("Unauthorized: Chat does not belong to user"); + } + return chat; } catch (error) { console.error("Failed to get chat by id:", error); return null; } }, });Follow-up: If any server-to-server caller needs unrestricted access, add a separate internalQuery (e.g., internal.chats.getByIdService) guarded by requireServiceKey.
86-112: saveChat should be service-key only or user-auth; pick one and enforce.Given prior learnings, this is intended for backend use. Enforce the key.
Apply:
- // Verify service role key - validateServiceKey(args.serviceKey); + // Verify service role key + requireServiceKey(args.serviceKey);Optionally, remove serviceKey entirely and require ctx.auth + ownership if this must be callable from clients.
117-189: updateChat auth model inconsistent; enforce chosen model.Same issue as saveChat. Either require service key (backend-only) or require ctx.auth and verify ownership.
Apply:
- // Verify service role key - validateServiceKey(args.serviceKey); + // Verify service role key + requireServiceKey(args.serviceKey);If switching to user-auth, drop serviceKey arg and verify owner like deleteChat.
37-39: RestrictgetChatByIdto authenticated users and remove theserviceKeyarg
- Verified that neither
api.chats.saveChatnorapi.chats.updateChatare ever imported or called in any.ts/.tsxclient code; all invocations live inlib/db/actions.ts(server-only) viaprocess.env.CONVEX_SERVICE_ROLE_KEY– so those mutations remain backend-only as intended.- However,
getChatByIdinconvex/chats.tsstill declaresargs: { serviceKey: v.optional(v.string()), id: v.string() }and usesvalidateServiceKeyalone, meaning callers without a key (i.e. unauthenticated clients) can fetch any chat by id.- To tighten security and align with our pattern:
• Remove theserviceKeyargument fromgetChatById.
• In the handler, requirectx.auth.userIdand call the existingverifyChatOwnershipinternal query before fetching.
• Throw an unauthorized error ifctx.authis missing or the chat doesn’t belong to the user.Locations to update:
- convex/chats.ts (around lines 37–39 and handler at ~67)
app/components/chat.tsx (2)
166-188: Switching to a chat with zero messages leaves stale messages.Early return on empty results prevents clearing local messages when the target chat has 0 messages.
Apply:
- useEffect(() => { - if (!paginatedMessages.results || paginatedMessages.results.length === 0) - return; - // Messages come from server in descending order, reverse for chronological display - const uiMessages = convertToUIMessages( - [...paginatedMessages.results].reverse(), - ); - // Merge strategy: Only sync from Convex if: - // 1. We have no local messages (initial load) - // 2. Convex has more messages than local (new messages from server) - // 3. Message IDs differ (switching chats or real-time updates) - const shouldSync = - messages.length === 0 || - uiMessages.length > messages.length || - (uiMessages.length > 0 && - messages.length > 0 && - uiMessages[0]?.id !== messages[0]?.id); - if (shouldSync) { - setMessages(uiMessages); - } - }, [paginatedMessages.results, setMessages, messages]); + useEffect(() => { + if (!paginatedMessages.results) return; + // Messages come from server in descending order, reverse for chronological display + const uiMessages = convertToUIMessages( + [...paginatedMessages.results].reverse(), + ); + // Sync when lengths differ or first id differs (covers switching) — also clears when server is empty + const shouldSync = + messages.length !== uiMessages.length || + (uiMessages[0]?.id !== messages[0]?.id); + if (shouldSync) { + setMessages(uiMessages); + } + }, [paginatedMessages.results, setMessages, messages]);
85-89: Potential auth mismatch for getChatById—confirm backend alignmentThe client‐side call in
app/components/chat.tsxis invokinguseQuery(api.chats.getChatById, …)under end-user credentials, whereas the server-side helper inlib/db/actions.tsstill calls the same API with aserviceKey. To safely expose this query to authenticated users (and remove reliance on the service-role key), you’ll need to update your query definition in the functions layer to enforce the correct ACL rules and drop the serviceKey requirement.• lib/db/actions.ts (lines 10–14):
export async function getChatById({ id }: { id: string }) { return convex.query(api.chats.getChatById, { serviceKey: process.env.CONVEX_SERVICE_ROLE_KEY!, id, }); }– Internal, privileged call using the service role key.
• app/components/chat.tsx (lines 85–89):
const chatData = useQuery( api.chats.getChatById, id || currentChatId ? { id: chatId } : "skip", );– Public client call under user auth.
Action items:
- Modify your
getChatByIdquery in the functions (e.g.functions/chats.ts) to validate that the requesting user is authorized and remove any dependency onserviceKey.- Ensure no other parts of the codebase still expect the serviceKey version of this query.
- Verify that row-level security (chat membership checks) is enforced in the query itself.
🧹 Nitpick comments (20)
app/globals.css (2)
95-110: Sidebar mask: remove duplicate stop and prep for Safari masksThere’s a duplicated “transparent 100%” stop. Also, ensure consumers set both mask-image and -webkit-mask-image for Safari.
Apply this diff to remove the duplicate stop:
--sidebar-mask: linear-gradient( 90deg, #000 0%, #000 95%, - transparent 100%, - transparent 100% + transparent 100% );Example usage (outside this file):
/* Example consumer */ .sidebar-title { -webkit-mask-image: var(--sidebar-mask); mask-image: var(--sidebar-mask); mask-mode: luminance; }(Optional a11y fallback)
@media (forced-colors: active) { .sidebar-title { -webkit-mask-image: none; mask-image: none; } }
156-170: Dark theme mask: same duplicate stopMirror the fix in dark theme.
Apply this diff:
--sidebar-mask: linear-gradient( 90deg, #000 0%, #000 95%, - transparent 100%, - transparent 100% + transparent 100% );app/hooks/useMessageScroll.ts (1)
10-29: Unify return type to Promise for ergonomic consumptionMixed boolean | Promise can complicate callers. Consider always returning a Promise.
Apply this diff:
- const scrollToBottom = useCallback( - (options?: { - force?: boolean; - instant?: boolean; - }): boolean | Promise<boolean> => { + const scrollToBottom = useCallback( + (options?: { + force?: boolean; + instant?: boolean; + }): Promise<boolean> => { if (options?.instant) { const scrollContainer = stickToBottom.scrollRef.current; - if (scrollContainer) { - scrollContainer.scrollTop = scrollContainer.scrollHeight; - } - return true; + if (!scrollContainer) return Promise.resolve(false); + scrollContainer.scrollTop = scrollContainer.scrollHeight; + return Promise.resolve(true); } - return stickToBottom.scrollToBottom({ + return Promise.resolve( + stickToBottom.scrollToBottom({ animation: "smooth", preserveScrollPosition: !options?.force, - }); + }), + ); }, [stickToBottom.scrollToBottom, stickToBottom.scrollRef], );app/contexts/GlobalState.tsx (2)
67-69: Signature mismatch: fromRoute param declared but not implementedEither remove the param from the type or accept it in the implementation to avoid confusion.
Apply this diff to accept (and ignore) the param for now:
- initializeChat: (chatId: string, fromRoute?: boolean) => void; + initializeChat: (chatId: string, fromRoute?: boolean) => void;And below (implementation):
- const initializeChat = useCallback((chatId: string) => { + const initializeChat = useCallback((chatId: string, _fromRoute?: boolean) => {
114-123: isSwitchingChats is set but never reset hereEnsure some consumer resets setIsSwitchingChats(false) after messages load, or expose a finalize helper in context to standardize this.
Optional helper (outside this hunk):
// In interface: completeChatSwitch: () => void; // In provider: const completeChatSwitch = useCallback(() => setIsSwitchingChats(false), []); // In value: completeChatSwitch,app/components/SidebarUserNav.tsx (3)
50-71: Use a button for the trigger for keyboard/a11yDiv as trigger harms semantics; switch to a button.
Apply this diff:
- <DropdownMenuTrigger asChild> - <div className="flex items-center gap-3 p-3 cursor-pointer hover:bg-sidebar-accent/50 rounded-md transition-colors"> + <DropdownMenuTrigger asChild> + <button + type="button" + className="flex items-center gap-3 p-3 hover:bg-sidebar-accent/50 rounded-md transition-colors" + > ... - </div> + </button>
21-23: Prefer router navigation to avoid full page reload on logoutUse Next.js router push/replace for smoother UX (unless server requires hard reload).
Apply this diff:
+import { useRouter } from "next/navigation"; ... const SidebarUserNav = () => { - const { user } = useAuth(); + const { user } = useAuth(); + const router = useRouter(); ... const handleSignOut = async () => { - window.location.href = "/logout"; + router.push("/logout"); };
91-94: DropdownMenuItem “variant” prop may not existIf your DropdownMenuItem doesn’t support variant, style via className instead.
Apply this diff if needed:
- <DropdownMenuItem onClick={handleSignOut} variant="destructive"> + <DropdownMenuItem + onClick={handleSignOut} + className="text-destructive focus:bg-destructive/10" + >app/components/SidebarHistory.tsx (3)
31-55: Rebind scroll listener on element changes + use passive listenerThe effect won’t reattach if
containerRef.currentswaps (e.g., overlay toggles). Also, prefer passive listeners for scroll.Apply:
- React.useEffect(() => { - const handleScroll = () => { + React.useEffect(() => { + const handleScroll = () => { if ( !containerRef?.current || !loadMore || paginationStatus !== "CanLoadMore" ) { return; } const { scrollTop, scrollHeight, clientHeight } = containerRef.current; // Check if we're near the bottom (within 100px) if (scrollTop + clientHeight >= scrollHeight - 100) { - loadMore(28); // Load 28 more chats + loadMore(PAGE_SIZE); // Load more chats } }; - const container = containerRef?.current; - if (container) { - container.addEventListener("scroll", handleScroll); - return () => container.removeEventListener("scroll", handleScroll); - } - }, [containerRef, loadMore, paginationStatus]); + const el = containerRef?.current; + if (!el) return; + el.addEventListener("scroll", handleScroll, { passive: true }); + return () => el.removeEventListener("scroll", handleScroll); + }, [containerRef?.current, loadMore, paginationStatus]);
9-20: Tighten prop types; avoidany[]; refine ref typingDefine a minimal chat shape and simplify ref typing.
-interface SidebarHistoryProps { - chats: any[]; +type ChatListItem = { _id: string; id: string; title: string }; +interface SidebarHistoryProps { + chats: ChatListItem[]; currentChatId: string | null; handleNewChat: () => void; paginationStatus?: | "LoadingFirstPage" | "CanLoadMore" | "LoadingMore" | "Exhausted"; loadMore?: (numItems: number) => void; - containerRef?: React.RefObject<HTMLDivElement | null>; + containerRef?: React.RefObject<HTMLDivElement>; }
22-29: De-duplicate magic number with a PAGE_SIZE constantPromote 28 to a constant for consistency with other components.
const SidebarHistory: React.FC<SidebarHistoryProps> = ({ @@ - if (scrollTop + clientHeight >= scrollHeight - 100) { - loadMore(28); // Load 28 more chats + if (scrollTop + clientHeight >= scrollHeight - 100) { + loadMore(PAGE_SIZE); }Add near the top (module scope):
+"use client"; + +const PAGE_SIZE = 28;Also applies to: 43-46
app/components/Messages.tsx (2)
127-135: Scroll listener may not attach on ref changes + use passive listenerIf
scrollRef.currentchanges after first render, the listener won’t rebind. Also use a passive listener.- useEffect(() => { - const scrollElement = scrollRef.current; - if (!scrollElement) return; - - scrollElement.addEventListener("scroll", handleScroll); - return () => scrollElement.removeEventListener("scroll", handleScroll); - }, [handleScroll]); + useEffect(() => { + const el = scrollRef.current; + if (!el) return; + el.addEventListener("scroll", handleScroll, { passive: true }); + return () => el.removeEventListener("scroll", handleScroll); + }, [scrollRef?.current, handleScroll]);
108-126: Guard against burst loadMore and centralize page sizeMinor: centralize 28 and keep the trigger minimal.
-import Loading from "@/components/ui/loading"; +import Loading from "@/components/ui/loading"; +const PAGE_SIZE = 28; @@ - if (scrollTop < 100) { - loadMore(28); // Load 28 more messages + if (scrollTop < 100) { + loadMore(PAGE_SIZE); }Also applies to: 121-124
app/components/Sidebar.tsx (1)
72-76: Avoid hardcoding initialNumItems across the appExtract PAGE_SIZE for consistency with SidebarHistory and Messages.
-import { usePaginatedQuery } from "convex/react"; +import { usePaginatedQuery } from "convex/react"; +const PAGE_SIZE = 28; @@ - const paginatedChats = usePaginatedQuery( + const paginatedChats = usePaginatedQuery( api.chats.getUserChats, user ? {} : "skip", - { initialNumItems: 28 }, + { initialNumItems: PAGE_SIZE }, );app/components/ChatItem.tsx (2)
18-22: Remove unusedisActiveprop; rely on global stateThe local prop is unused; prefer single source of truth.
ChatItem:
-interface ChatItemProps { +interface ChatItemProps { id: string; title: string; - isActive?: boolean; } @@ -const ChatItem: React.FC<ChatItemProps> = ({ id, title, isActive = false }) => { +const ChatItem: React.FC<ChatItemProps> = ({ id, title }) => {SidebarHistory:
<ChatItem key={chat._id} id={chat.id} title={chat.title} - isActive={currentChatId === chat.id} />Also applies to: 24-24, 95-101
55-70: Add delete confirmation + user feedbackPrevent accidental deletions and surface outcomes.
+import { toast } from "sonner"; @@ - const handleDelete = async (e: React.MouseEvent) => { + const handleDelete = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); try { + if (!window.confirm("Delete this chat? This cannot be undone.")) return; await deleteChat({ chatId: id }); + toast.success("Chat deleted"); // If we're deleting the currently active chat, navigate to home if (isCurrentlyActive) { initializeNewChat(); router.push("/"); } } catch (error) { console.error("Failed to delete chat:", error); + toast.error("Failed to delete chat"); } };I can swap this to an AlertDialog if you prefer.
convex/messages.ts (1)
74-80: Return shape: prefercontinueCursor: undefinedfor empty resultsAvoid inventing an empty string; align with Convex’s paginated types.
return { page: [], isDone: true, - continueCursor: "", + continueCursor: undefined, }; @@ return { page: [], isDone: true, - continueCursor: "", + continueCursor: undefined, };Also applies to: 99-105
convex/chats.ts (3)
191-229: Pagination return shape OK; minor polish.
- Comment says “Transform the page…” but code returns result untouched. Either transform or update the comment.
- Consider adding an explicit returns schema for consistency.
Possible tidy:
- // Transform the page data to include only needed fields - return result; + // Return paginated chats for the authenticated user + return result;
233-281: deleteChat correctness OK; improve scalability and reduce double fetch.
- You fetch the chat twice (verifyChatOwnership + query). Return the doc’s _id from verifyChatOwnership to avoid re-query, or inline the ownership check here.
- For large chats, deleting after collect() can load many docs at once. Prefer chunked pagination.
Apply:
- // Verify chat ownership - await ctx.runQuery(internal.chats.verifyChatOwnership, { + // Verify chat ownership + await ctx.runQuery(internal.chats.verifyChatOwnership, { chatId: args.chatId, userId: user.subject, }); - - // Find the chat - const chat = await ctx.db - .query("chats") - .withIndex("by_chat_id", (q) => q.eq("id", args.chatId)) - .first(); + // Find the chat (to get _id) + const chat = await ctx.db + .query("chats") + .withIndex("by_chat_id", (q) => q.eq("id", args.chatId)) + .first(); @@ - // Delete all messages associated with this chat - const messages = await ctx.db - .query("messages") - .withIndex("by_chat_id", (q) => q.eq("chat_id", args.chatId)) - .collect(); - - for (const message of messages) { - await ctx.db.delete(message._id); - } + // Delete messages in chunks to limit memory usage + while (true) { + const page = await ctx.db + .query("messages") + .withIndex("by_chat_id", (q) => q.eq("chat_id", args.chatId)) + .order("asc") + .take(256); + if (page.length === 0) break; + for (const m of page) { + await ctx.db.delete(m._id); + } + }Optional: Change verifyChatOwnership to return the chat _id so the extra chat fetch isn’t needed.
12-32: Minor: internal verifyChatOwnership could return the chat id to avoid re-query.Apply:
- returns: v.null(), + returns: v.object({ _id: v.id("chats") }), @@ - return null; + return { _id: chat._id };Then use the returned _id in deleteChat.
📜 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 (13)
app/components/ChatItem.tsx(3 hunks)app/components/Messages.tsx(4 hunks)app/components/Sidebar.tsx(6 hunks)app/components/SidebarHistory.tsx(1 hunks)app/components/SidebarUserNav.tsx(1 hunks)app/components/UserDropdownMenu.tsx(0 hunks)app/components/chat.tsx(9 hunks)app/contexts/GlobalState.tsx(6 hunks)app/globals.css(2 hunks)app/hooks/useChatHandlers.ts(1 hunks)app/hooks/useMessageScroll.ts(1 hunks)convex/chats.ts(3 hunks)convex/messages.ts(4 hunks)
💤 Files with no reviewable changes (1)
- app/components/UserDropdownMenu.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-27T12:38:06.615Z
Learnt from: RostyslavManko
PR: hackerai-tech/hackerai#14
File: convex/chats.ts:4-24
Timestamp: 2025-08-27T12:38:06.615Z
Learning: Convex functions with serviceKey parameters are designed for backend service-to-service communication and don't require ctx.auth.getUserIdentity() checks. The serviceKey validation against process.env.CONVEX_SERVICE_ROLE_KEY provides sufficient authentication for these backend operations.
Applied to files:
convex/chats.ts
🧬 Code graph analysis (9)
app/components/SidebarUserNav.tsx (2)
components/ui/dropdown-menu.tsx (6)
DropdownMenu(242-242)DropdownMenuTrigger(244-244)DropdownMenuContent(245-245)DropdownMenuLabel(247-247)DropdownMenuSeparator(252-252)DropdownMenuItem(248-248)components/ui/avatar.tsx (3)
Avatar(53-53)AvatarImage(53-53)AvatarFallback(53-53)
app/components/SidebarHistory.tsx (1)
components/ui/loading.tsx (1)
Loading(7-14)
app/components/Messages.tsx (1)
components/ui/loading.tsx (1)
Loading(7-14)
app/hooks/useChatHandlers.ts (1)
app/contexts/GlobalState.tsx (1)
useGlobalState(202-208)
app/components/ChatItem.tsx (4)
app/contexts/GlobalState.tsx (1)
useGlobalState(202-208)hooks/use-mobile.ts (1)
useIsMobile(5-21)convex/chats.ts (1)
deleteChat(233-281)convex/_generated/api.js (2)
api(21-21)api(21-21)
app/components/Sidebar.tsx (1)
app/contexts/GlobalState.tsx (1)
useGlobalState(202-208)
convex/messages.ts (1)
convex/_generated/server.js (2)
query(29-29)query(29-29)
app/components/chat.tsx (4)
app/contexts/GlobalState.tsx (1)
useGlobalState(202-208)lib/utils.ts (1)
convertToUIMessages(34-42)app/hooks/useMessageScroll.ts (1)
useMessageScroll(4-38)app/hooks/useChatHandlers.ts (1)
useChatHandlers(20-148)
convex/chats.ts (2)
convex/_generated/server.js (4)
query(29-29)query(29-29)mutation(49-49)mutation(49-49)convex/_generated/api.js (2)
internal(22-22)internal(22-22)
🔇 Additional comments (25)
app/hooks/useMessageScroll.ts (3)
5-8: Good defaults for stick-to-bottom behavior
28-29: Confirm dependency stabilityIf useStickToBottom returns stable method refs, this is fine. If not, depend on the object or expose the method via useMemo in the lib to avoid stale closures.
32-37: Return shape looks goodForwarding refs and stopScroll aligns with the new API.
app/contexts/GlobalState.tsx (6)
31-40: New chat state flags: LGTM
86-89: Initial values are sane
110-112: resetChat now clears activity/fetch flagsNice catch to avoid stale fetches.
125-132: initializeNewChat: LGTM
166-172: Exposing new flags and setters: LGTM
191-193: New init methods exposed: LGTMapp/hooks/useChatHandlers.ts (1)
29-40: No remaining prop usages; hook callers updated correctlyVerified that all invocations of useChatHandlers only pass the expected chatId and messages arguments—none of the deprecated flags (
shouldFetchMessages,setShouldFetchMessages,setHasActiveChat, etc.) are still being supplied as props. Specifically:
- app/components/chat.tsx at line 204: only
chatIdandmessagesare passed to useChatHandlers (confirmed via ripgrep)- The removed flags now live solely in GlobalState.tsx and are consumed internally by the hook
Since no callers remain that pass the old props, the refactor is complete and changes can be approved.
app/components/SidebarUserNav.tsx (1)
53-59: Verify WorkOS user fieldsConfirm profilePictureUrl and email property names match the WorkOS user object to avoid undefined values.
app/components/SidebarHistory.tsx (1)
94-101: Chat list rendering looks goodKeys, props, and active state wiring are correct.
app/components/Messages.tsx (2)
142-147: Top loading indicator integration is solidGood UX signal during pagination.
82-89: Confirm message ordering vs. UI expectationsBackend returns newest-first; this component maps
messagesas-is. Ensure upstream normalizes order to chronological for chat UX and forfindLastAssistantMessageIndexassumptions.Would you like me to add a small adapter to reverse pages before render if needed?
app/components/Sidebar.tsx (1)
114-122: SidebarHistory wiring LGTMPassing
results,status,loadMore, and the scroll container ref aligns with the pagination design.Also applies to: 145-153
app/components/ChatItem.tsx (1)
50-53: ConfirminitializeChat(id, true)signature/semanticsEnsure the second arg is expected and correct; otherwise align with the new API.
convex/messages.ts (2)
55-59: Args validation for pagination is correctUsing
paginationOptsValidatoris the right approach.
82-89: Newest-first ordering: verify UI handles display orderSince results are
desc, ensure the client renders chronologically or reverses as needed to keep newest at bottom and avoid loader/index logic drift.I can add a small adapter on the client to normalize order; say the word.
convex/chats.ts (1)
3-5: Heads-up: import of paginationOptsValidator and internal is correct. LGTM.app/components/chat.tsx (6)
77-83: Paginated messages integration looks good.Using usePaginatedQuery with initialNumItems: 28 and passing status/loadMore to Messages is clean.
150-153: Title sync precedence is correct. LGTM.Server title always winning avoids UI drift.
194-201: Instant scroll-on-switch is correct.Good UX improvement with force+instant then resetting the flag.
268-271: Messages props: new pagination contract wired correctly.Passing paginationStatus/loadMore/isSwitchingChats aligns with the updated Messages signature.
235-236: Sidebar replacement LGTM.MainSidebar integration on desktop and mobile overlay looks consistent.
54-75: Initialization helpers are idempotent and guards prevent redundant callsI’ve confirmed that both initializeChat and initializeNewChat simply reset global state to fixed values—repeated invocations leave state unchanged (e.g., setting flags to true/false or clearing arrays to empty) and thus are idempotent. Combined with the
hasInitializedRouteIdandhasInitializedNewChatguards in the effect, redundant initializations cannot occur. No further changes are needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 15
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
app/api/chat/route.ts (2)
36-38: Propagate client abort to the stream.Currently logs only; abort the controller so downstream respects cancellation.
- req.signal.addEventListener("abort", () => { - console.log("Request aborted"); - }); + req.signal.addEventListener("abort", () => { + console.log("Request aborted"); + controller.abort("client aborted"); + });
41-53: Validate request payload and guard empty messages.Prevent crashes in
handleInitialChatAndUserMessagewhenmessagesis empty andregenerateis false.} = await req.json(); - const { userId, isPro } = await getUserIDAndPro(req); + if (!Array.isArray(messages)) { + throw new ChatSDKError( + "bad_request:chat", + "'messages' must be an array." + ); + } + if (!regenerate && messages.length === 0) { + throw new ChatSDKError( + "bad_request:chat", + "'messages' cannot be empty unless regenerate=true." + ); + } + + const { userId, isPro } = await getUserIDAndPro(req);Also applies to: 61-67
app/hooks/useChatHandlers.ts (1)
51-60: Use hasActiveChat instead of messages.length === 0This avoids misclassification when messages are not yet fetched.
- if (messages.length === 0) { + if (!hasActiveChat) { setChatTitle(null); setCurrentChatId(chatId); window.history.replaceState({}, "", `/c/${chatId}`); if (!shouldFetchMessages) { setShouldFetchMessages(true); } setHasActiveChat(true); }
🧹 Nitpick comments (13)
.env.local.example (2)
41-42: Rate limit keys: document defaults.Consider adding commented example values so developers know expected magnitudes, e.g., PRO_RATE_LIMIT_REQUESTS=200, FREE_RATE_LIMIT_REQUESTS=50.
53-53: Add trailing newline.dotenv-linter flagged missing EOF newline. Apply:
-# STRIPE_SECRET_KEY= +# STRIPE_SECRET_KEY= +lib/auth/get-user-id.ts (1)
12-22: De-duplicate session fetch: implement getUserID via getUserIDAndPro.Avoid repeating authkit/session logic and keep behavior in one place.
-export const getUserID = async (req: NextRequest): Promise<string> => { - try { - const { authkit } = await import("@workos-inc/authkit-nextjs"); - const { session } = await authkit(req); - - if (!session?.user?.id) { - throw new ChatSDKError("unauthorized:auth"); - } - - return session.user.id; - } catch (error) { - if (error instanceof ChatSDKError) { - throw error; - } - - console.error("Failed to get user session:", error); - throw new ChatSDKError("unauthorized:auth"); - } -}; +export const getUserID = async (req: NextRequest): Promise<string> => { + const { userId } = await getUserIDAndPro(req); + return userId; +};Also applies to: 40-43
app/api/chat/route.ts (1)
98-116: Optional: guard analytics errors.Wrap PostHog capture with try/catch to avoid impacting tool calls.
- if (posthog) { - posthog.capture({ - distinctId: userId, - event: "hackerai-" + chunk.chunk.toolName, - }); - } + if (posthog) { + try { + posthog.capture({ + distinctId: userId, + event: "hackerai-" + chunk.chunk.toolName, + }); + } catch {} + }app/components/SidebarUpgrade.tsx (1)
61-67: Accessibility: reflect loading on the button.Expose loading state to assistive tech.
<Button onClick={handleSubscribe} disabled={loading} + aria-busy={loading} + aria-live="polite" className="w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 text-white border-0 shadow-sm" size="sm" >app/components/SidebarUserNav.tsx (1)
96-100: Verify DropdownMenuItem supportsvariant="destructive".If not supported in your UI lib, switch to a class-based style.
- <DropdownMenuItem onClick={handleSignOut} variant="destructive"> + <DropdownMenuItem + onClick={handleSignOut} + className="text-destructive focus:text-destructive" + > <LogOut className="mr-2 h-4 w-4" /> <span>Log out</span> </DropdownMenuItem>app/hooks/useChatHandlers.ts (2)
97-107: Add error handling around regenerationNetwork or mutation failures currently throw and leave UI in an odd state. Wrap in try/catch and log.
- const handleRegenerate = async () => { - await deleteLastAssistantMessage({ chatId }); - - regenerate({ - body: { - mode, - todos, - regenerate: true, - }, - }); - }; + const handleRegenerate = async () => { + try { + await deleteLastAssistantMessage({ chatId }); + regenerate({ + body: { + mode, + todos, + regenerate: true, + }, + }); + } catch (error) { + console.error("Failed to regenerate:", error); + } + };
123-127: Preserve non-text parts when editing a messageOverwriting parts to a single text part can drop attachments/inline elements. Update the first text part if present; otherwise fallback to a single text part.
- updatedMessages[editedMessageIndex] = { - ...updatedMessages[editedMessageIndex], - parts: [{ type: "text", text: newContent }], - }; + const old = updatedMessages[editedMessageIndex]; + updatedMessages[editedMessageIndex] = { + ...old, + parts: + old.parts?.map((p, i) => + p.type === "text" && i === 0 ? { ...p, text: newContent } : p, + ) ?? [{ type: "text", text: newContent }], + };app/api/subscribe/route.ts (1)
24-31: Selecting the first membership may bind the wrong orgIf a user belongs to multiple orgs, reusing the first membership is arbitrary. Prefer an org already linked to a Stripe customer (stripeCustomerId) or allow the client to specify target org.
I can add a filter to prefer organizations with organization.stripeCustomerId or expose org selection from the UI.
app/contexts/GlobalState.tsx (2)
135-142: Deduplicate chat reset logic via initializeNewChatresetChat largely mirrors initializeNewChat. Reuse it and keep a single source of truth.
const resetChat = () => { - setInput(""); - setChatTitle(null); - setTodos([]); - setIsTodoPanelExpanded(false); - setHasActiveChat(false); - setShouldFetchMessages(false); + setInput(""); + initializeNewChat(); };
187-225: Stabilize context value to reduce downstream re-rendersvalue is recreated every render. Wrap it in useMemo or split the context to hot/cold slices if perf becomes an issue.
I can provide a targeted useMemo refactor if you see render thrash in the sidebar/messages trees.
app/components/Sidebar.tsx (2)
31-38: Add accessible labels to the sidebar toggle button<Button variant="ghost" size="sm" className="h-7 w-7 p-0" onClick={handleCloseSidebar} + aria-label="Close sidebar" >
41-49: Add accessible label to “New Chat” button<Button onClick={handleNewChat} variant="ghost" size="sm" className="h-8 w-8 p-0" title="New Chat" + aria-label="New chat" >
📜 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 ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (15)
.env.local.example(1 hunks)README.md(0 hunks)app/api/chat/route.ts(4 hunks)app/api/stripe.ts(1 hunks)app/api/subscribe/route.ts(1 hunks)app/api/workos.ts(1 hunks)app/components/Sidebar.tsx(6 hunks)app/components/SidebarUpgrade.tsx(1 hunks)app/components/SidebarUserNav.tsx(1 hunks)app/contexts/GlobalState.tsx(7 hunks)app/hooks/useChatHandlers.ts(1 hunks)lib/auth/get-user-id.ts(1 hunks)lib/auth/refresh-session.ts(1 hunks)lib/rate-limit.ts(2 hunks)package.json(2 hunks)
💤 Files with no reviewable changes (1)
- README.md
🧰 Additional context used
🧬 Code graph analysis (8)
app/components/SidebarUpgrade.tsx (1)
app/contexts/GlobalState.tsx (1)
useGlobalState(234-240)
app/components/SidebarUserNav.tsx (1)
app/contexts/GlobalState.tsx (1)
useGlobalState(234-240)
app/api/subscribe/route.ts (1)
lib/auth/get-user-id.ts (1)
getUserID(12-30)
lib/auth/get-user-id.ts (1)
lib/errors.ts (1)
ChatSDKError(36-74)
app/contexts/GlobalState.tsx (3)
types/chat.ts (3)
SidebarContent(24-24)ChatMode(3-3)Todo(38-42)lib/utils/todo-utils.ts (1)
mergeTodos(11-37)lib/auth/refresh-session.ts (1)
refreshAuthkitSession(5-8)
app/hooks/useChatHandlers.ts (1)
app/contexts/GlobalState.tsx (1)
useGlobalState(234-240)
app/api/chat/route.ts (4)
lib/auth/get-user-id.ts (1)
getUserIDAndPro(40-64)lib/rate-limit.ts (1)
checkRateLimit(6-68)lib/db/actions.ts (1)
handleInitialChatAndUserMessage(66-129)lib/ai/tools/index.ts (1)
createTools(17-73)
app/components/Sidebar.tsx (1)
app/contexts/GlobalState.tsx (1)
useGlobalState(234-240)
🪛 dotenv-linter (3.3.0)
.env.local.example
[warning] 53-53: [EndingBlankLine] No blank line at the end of the file
(EndingBlankLine)
🔇 Additional comments (8)
lib/auth/get-user-id.ts (1)
51-56: Confirm entitlement shape—no change needed
session.entitlementsis exposed as astring[]per AuthKit, so usingsession.entitlements || []and checkingincludesis correct.lib/rate-limit.ts (1)
33-41: No changes needed:resetis epoch ms, noresetAfter, and “5 h” is valid.Upstash’s
limit().resetreturns a Unix timestamp in milliseconds, so the current calculation oftimeDiff,hours, andminutesis correct. There is noresetAfterproperty on the API—compute any TTL fromresetif needed. The window string"5 h"is a valid format.app/api/chat/route.ts (1)
55-60: LGTM on identity propagation and rate-limiting usage.Using
getUserIDAndProand passinguserIdconsistently through the flow looks good.Also applies to: 69-77, 98-107
app/components/SidebarUpgrade.tsx (1)
52-57: LGTM on conditional rendering for auth/pro.Rendering nothing for unauthenticated or already-pro users is appropriate.
Also applies to: 59-83
app/components/SidebarUserNav.tsx (1)
17-25: LGTM on user display and status.Initials/name fallbacks and “Pro/Free” status wiring to global state look clean.
Also applies to: 66-73
app/api/subscribe/route.ts (1)
37-41: Confirm WorkOS role slug"admin" must exist as a role slug in your WorkOS project. If it doesn’t, membership creation will fail at runtime.
Would you like me to make the role configurable via env (e.g., WORKOS_DEFAULT_ROLE_SLUG) and add a fallback?
app/contexts/GlobalState.tsx (1)
144-154: isSwitchingChats never reset hereYou set setIsSwitchingChats(true) but don’t clear it in this provider. Ensure some consumer resets it to false after initial messages load; otherwise components depending on it may degrade UX.
I can wire a small hook in Messages.tsx to clear it after the first successful fetch if you want.
app/components/Sidebar.tsx (1)
73-77: Skip pattern for usePaginatedQuery is likely incorrectPassing the string "skip" as args is probably not supported by Convex and may cause type/runtime errors. Prefer undefined (or the official skip token, if provided by your Convex version).
const paginatedChats = usePaginatedQuery( api.chats.getUserChats, - user ? {} : "skip", + user ? {} : undefined, { initialNumItems: 28 }, );If Convex exposes a skip symbol, use that instead (e.g., import { skipToken } from the lib).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
♻️ Duplicate comments (2)
lib/auth/get-user-id.ts (1)
102-105: Harden entitlement parsing and make plan key configurableCoerce to a string array and read the plan key from env to tolerate shape changes and renames.
- // Check if user has pro entitlements - const entitlements = session.entitlements || []; - const isPro = entitlements.includes("pro-monthly-plan"); + // Check if user has pro entitlements + const PRO_ENTITLEMENT_KEY = + process.env.PRO_ENTITLEMENT_KEY ?? "pro-monthly-plan"; + const rawEntitlements = (session as any)?.entitlements; + const entitlements: string[] = Array.isArray(rawEntitlements) + ? rawEntitlements.map(String) + : []; + const isPro = entitlements.includes(PRO_ENTITLEMENT_KEY);app/components/SidebarUserNav.tsx (1)
58-60: Use a semantic button for the trigger.Replace the clickable div with a
<button>to improve accessibility and keyboard support.- <DropdownMenuTrigger asChild> - <div className="flex items-center gap-3 p-3 cursor-pointer hover:bg-sidebar-accent/50 rounded-md transition-colors"> + <DropdownMenuTrigger asChild> + <button + type="button" + aria-label="User menu" + className="flex items-center gap-3 p-3 cursor-pointer hover:bg-sidebar-accent/50 rounded-md transition-colors" + > … - </div> + </button>
🧹 Nitpick comments (14)
lib/auth/get-user-id.ts (2)
3-3: Remove stale commented importDrop the commented WorkOS import to avoid confusion.
-// import { WorkOS } from "@workos-inc/node";
41-82: Delete large commented-out legacy implementationDead code adds noise and risks divergence. Remove or move to internal docs.
-// export const getUserIDAndPro = async ( -// req: NextRequest, -// ): Promise<{ userId: string; isPro: boolean }> => { -// ... -// };app/api/entitlements/route.ts (2)
31-36: Mark response as non-cacheableAvoid stale entitlement states in intermediaries.
- const response = NextResponse.json({ - entitlements: entitlements || [], - hasProPlan, - }); + const response = NextResponse.json( + { entitlements: Array.isArray(entitlements) ? entitlements : [], hasProPlan }, + { headers: { "Cache-Control": "no-store" } }, + );
39-44: Tighten cookie optionsKeep httpOnly/sameSite; make secure conditional for local dev and set path.
- response.cookies.set("wos-session", sealedSession, { - httpOnly: true, - sameSite: "lax", - secure: true, - }); + response.cookies.set("wos-session", sealedSession, { + httpOnly: true, + sameSite: "lax", + secure: process.env.NODE_ENV === "production", + path: "/", + });app/contexts/GlobalState.tsx (3)
73-75: Align initializeChat signature with type (second param)Either drop
fromRoutefrom the type or accept it in the impl to avoid confusion at call sites.- initializeChat: (chatId: string, fromRoute?: boolean) => void; + initializeChat: (chatId: string, fromRoute?: boolean) => void; @@ - const initializeChat = useCallback((chatId: string) => { + const initializeChat = useCallback((chatId: string, _fromRoute?: boolean) => { setIsSwitchingChats(true);Also applies to: 163-170
110-149: Prevent state updates after unmount in entitlement fetchAdd a cancellation flag.
useEffect(() => { - const checkProPlan = async () => { + let cancelled = false; + const checkProPlan = async () => { if (user) { setIsCheckingProPlan(true); try { const response = await fetch("/api/entitlements", { credentials: "include", // Ensure cookies are sent }); @@ - const data = await response.json(); - setHasProPlan(data.hasProPlan || false); + const data = await response.json(); + if (!cancelled) setHasProPlan(!!data.hasProPlan); } catch (error) { console.error( "💥 [GlobalState] Failed to fetch entitlements:", error, ); - setHasProPlan(false); + if (!cancelled) setHasProPlan(false); } finally { - setIsCheckingProPlan(false); + if (!cancelled) setIsCheckingProPlan(false); } } else { setHasProPlan(false); setIsCheckingProPlan(false); } }; checkProPlan(); + return () => { + cancelled = true; + }; }, [user]);
172-178: Also reset isSwitchingChats in initializeNewChatSame rationale as resetChat.
const initializeNewChat = useCallback(() => { + setIsSwitchingChats(false); setCurrentChatId(null); setShouldFetchMessages(false); setHasActiveChat(false); setTodos([]); setIsTodoPanelExpanded(false); }, []);app/components/ChatHeader.tsx (2)
84-101: Use Next.js router for client navigation instead of window.locationThis preserves SPA behavior and avoids full reloads.
-import React from "react"; +import React from "react"; +import { useRouter } from "next/navigation"; @@ - const handleSignIn = () => { - window.location.href = "/login"; - }; - - const handleSignUp = () => { - window.location.href = "/signup"; - }; + const router = useRouter(); + const handleSignIn = () => router.push("/login"); + const handleSignUp = () => router.push("/signup");Also applies to: 143-161
58-78: DRY the duplicated upgrade CTA (desktop/mobile)Extract the button into a small local component to reduce duplication and keep styles in sync.
Example (add near top of file):
const UpgradeCta: React.FC<{ loading: boolean; onClick: () => void; size?: "sm" | "default"; }> = ({ loading, onClick, size = "default" }) => ( <Button onClick={onClick} disabled={loading} className="flex items-center gap-1 rounded-full py-2 ps-2.5 pe-3 text-sm font-medium bg-[#F1F1FB] text-[#5D5BD0] hover:bg-[#E4E4F6] dark:bg-[#373669] dark:text-[#DCDBF6] dark:hover:bg-[#414071] border-0 transition-all duration-200" size={size} > {loading ? ( <> <Loader2 className={size === "sm" ? "mr-1 h-3 w-3 animate-spin" : "mr-2 h-4 w-4 animate-spin"} /> Upgrading... </> ) : ( <> <Sparkle className={size === "sm" ? "mr-1 h-3 w-3 fill-current" : "mr-2 h-4 w-4 fill-current"} /> Upgrade to Pro </> )} </Button> );Then replace both inline button blocks with:
<UpgradeCta loading={upgradeLoading} onClick={handleUpgrade} size="default" />and
<UpgradeCta loading={upgradeLoading} onClick={handleUpgrade} size="sm" />Also applies to: 119-139
lib/actions/billing-portal.ts (2)
3-7: Prefer SDK over manual fetch to WorkOS.If
app/api/workos.tsexports the official WorkOS SDK client, use its Organizations API (e.g.,workos.organizations.getOrganization(organizationId)) to avoid relying onbaseURLinternals and hand-rolled headers. This reduces surface for subtle bugs and keeps auth handling consistent. I can provide a patch once I see the client shape inapp/api/workos.ts.
19-23: Hardenreturn_urlhandling.Build URLs with
new URL()to avoid malformed returns and double slashes; also consider preserving the current pathname when relevant (e.g., billing/account page) to improve UX after returning from Stripe.app/components/SidebarUserNav.tsx (3)
115-118:variant="destructive"likely unsupported onDropdownMenuItem.Shadcn’s
DropdownMenuItemdoesn’t accept avariantprop. Use a class or a dedicatedDestructivestyle token.- <DropdownMenuItem onClick={handleSignOut} variant="destructive"> + <DropdownMenuItem onClick={handleSignOut} className="text-destructive focus:text-destructive"> <LogOut className="mr-2 h-4 w-4" /> <span>Log out</span> </DropdownMenuItem>
29-31: Prefer auth provider sign-out over hard redirect.If WorkOS/AuthKit exposes a sign-out helper, use it to clear session cookies and revoke tokens; otherwise a GET to
/logoutmust exist server-side.- const handleSignOut = async () => { - window.location.href = "/logout"; - }; + const handleSignOut = async () => { + window.location.href = "/logout"; // replace with SDK signOut() if available + };
21-28: Edge case: Pro label may flicker while checking entitlements.While
isCheckingProPlanis true, consider rendering a skeleton/spinner or deferring the "Pro/Free" label to avoid brief incorrect status.
📜 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 (10)
app/api/entitlements/route.ts(1 hunks)app/api/subscribe/route.ts(1 hunks)app/components/ChatHeader.tsx(4 hunks)app/components/Sidebar.tsx(6 hunks)app/components/SidebarUserNav.tsx(1 hunks)app/contexts/GlobalState.tsx(6 hunks)app/hooks/useUpgrade.ts(1 hunks)lib/actions/billing-portal.ts(1 hunks)lib/auth/get-user-id.ts(2 hunks)lib/moderation.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- app/api/subscribe/route.ts
- app/components/Sidebar.tsx
🧰 Additional context used
🧬 Code graph analysis (5)
app/components/SidebarUserNav.tsx (3)
app/contexts/GlobalState.tsx (1)
useGlobalState(251-257)app/hooks/useUpgrade.ts (1)
useUpgrade(5-50)lib/actions/billing-portal.ts (1)
redirectToBillingPortal(8-26)
lib/actions/billing-portal.ts (2)
app/api/workos.ts (1)
workos(7-7)app/api/stripe.ts (1)
stripe(5-5)
lib/auth/get-user-id.ts (1)
lib/errors.ts (1)
ChatSDKError(36-74)
app/contexts/GlobalState.tsx (2)
types/chat.ts (3)
SidebarContent(24-24)ChatMode(3-3)Todo(38-42)lib/utils/todo-utils.ts (1)
mergeTodos(11-37)
app/components/ChatHeader.tsx (2)
app/contexts/GlobalState.tsx (1)
useGlobalState(251-257)app/hooks/useUpgrade.ts (1)
useUpgrade(5-50)
🔇 Additional comments (2)
lib/moderation.ts (1)
33-33: LGTM: error-level log is appropriate for empty moderation resultsSeverity bump is sensible in this failure path.
app/components/SidebarUserNav.tsx (1)
108-113: Remove config verification — Server Actions stable by default.
Next.js 15.5.0 ships with stable Server Actions enabled by default; noexperimental.serverActionsflag is required (nextjs.org, dev.to)Likely an incorrect or invalid review comment.
Summary by CodeRabbit
New Features
Improvements
Bug Fixes
Documentation