Should this retry on 429 as well? The gateway rate-limits bursts after a deploy. cc @tomasz
Comment Thread
Threaded review comments with replies, @mentions, edit and delete, reactions and resolve.
src/payments/retry.ts ยท line 42
NH Noor HaddadTW Tomasz WierzbaYes, but only with the Retry-After header. Otherwise we hammer it again.
RK Riya Kapoor(edited)Added that in the last commit. @noor can you take another look?
Keyboard
- CtrlEnter
- Post, or save an edit
- Escape
- Cancel a reply, edit or delete
The CLI also installs the npm packages and registry items it builds on, and reads the style in your components.json to pick the Base UI or Radix build.
"use client"
import { CommentThread } from "@/components/ui/comment-thread"
const me = { id: "riya", name: "Riya Kapoor" }
const people = [{ value: "noor", label: "Noor Haddad" }, { value: "riya", label: "Riya Kapoor" }]
export function ReviewThread() {
return (
<CommentThread
currentUser={me}
mentionSuggestions={people}
defaultComments={[
{ id: "1", author: { id: "noor", name: "Noor Haddad" }, body: "Retry on 429 too? cc @riya", createdAt: "2026-11-04T09:00:00Z" },
]}
onReply={async (parentId, body, mentions) => {
const response = await fetch("/api/comments", { method: "POST", body: JSON.stringify({ parentId, body, mentions }) })
if (!response.ok) throw new Error("Could not save the comment")
}}
/>
)
}CommentThread
A single review thread. Top-level comments can have one level of replies; replying to a reply adds to the same top-level comment and starts the draft with @author when the author's id is a mention value. Ctrl or Cmd+Enter submits, Escape cancels an edit, a reply or a delete confirmation, and the reaction picker moves with the Left and Right arrow keys, Home and End.
| Prop | Type | Default |
|---|---|---|
commentsComment[]Controlled comments. Top-level comments render in array order, each followed by its replies in array order. | Comment[] | |
defaultCommentsComment[]Default[]Initial comments when uncontrolled. | Comment[] | [] |
onCommentsChange(comments: Comment[]) => voidCalled with the full list after a comment is posted, edited or deleted, or a reaction changes. | (comments: Comment[]) => void | |
currentUserCommentAuthorThe person writing. Edit and delete appear only on comments whose author.id matches. Leave it out for a read-only thread with no composer, actions or reaction toggles. | CommentAuthor | |
mentionSuggestionsMentionOption[]Default[]People offered after @ in the composer. An @value in a body is highlighted only when value is in this list. | MentionOption[] | [] |
reactionsReactionOption[]DefaultQUICK_REACTIONSEmoji offered in the reaction picker, each { emoji, label }. The default is ๐ ๐ ๐ ๐ ๐ โค๏ธ ๐ ๐. | ReactionOption[] | QUICK_REACTIONS |
resolvedbooleanControlled resolved state. A resolved thread collapses to a summary row with Show thread, plus Reopen when currentUser is set. | boolean | |
defaultResolvedbooleanDefaultfalseInitial resolved state when uncontrolled. | boolean | false |
onResolvedChange(resolved: boolean) => voidCalled when someone resolves or reopens the thread. | (resolved: boolean) => void | |
resolvedByCommentAuthorShown as Resolved by name. Falls back to currentUser when the thread was resolved in this session. | CommentAuthor | |
onReply(parentId: string | null, body: string, mentions: string[]) => void | Promise<void>Called with the trimmed body and the mentioned values. parentId is null for a new top-level comment. The comment is added after the promise resolves; if it rejects, the draft stays and an error shows. | (parentId: string | null, body: string, mentions: string[]) => void | Promise<void> | |
onEdit(id: string, body: string, mentions: string[]) => void | Promise<void>Called when an edit is saved with different text. Same promise handling as onReply. | (id: string, body: string, mentions: string[]) => void | Promise<void> | |
onDelete(id: string) => void | Promise<void>Called after the delete is confirmed. A comment with replies becomes a deleted placeholder; one without is removed. If the promise rejects, the comment stays. | (id: string) => void | Promise<void> | |
onReactionToggle(id: string, emoji: string) => voidCalled after the current user adds or removes a reaction. | (id: string, emoji: string) => void | |
nowDateReference time for relative timestamps. Without it the thread uses the current time and updates every minute. | Date | |
localestringLocale for relative and absolute timestamps. Defaults to the browser locale. | string | |
aria-labelstringDefault"Comment thread"Accessible name of the thread region. | string | "Comment thread" |
Other props go to the <section>.
Comment
One comment or reply.
| Prop | Type | Default |
|---|---|---|
idRequiredstringUnique id. | string | |
parentIdstring | nullThe comment this replies to. Missing parents make it a top-level comment. | string | null | |
authorRequiredCommentAuthor{ id: string; name: string; avatarUrl?: string }. Initials show when there is no avatarUrl or it fails to load. | CommentAuthor | |
bodyRequiredstringPlain text. Line breaks are kept and HTML is shown as text. | string | |
createdAtRequiredDate | stringAny value new Date() accepts. An invalid date hides the timestamp. | Date | string | |
editedAtDate | stringShows (edited) with the time in its tooltip. | Date | string | |
deletedbooleanShows a deleted placeholder while the comment has replies, and hides it otherwise. | boolean | |
reactions{ emoji: string; userIds: string[] }[]Each emoji and who reacted. Names come from the thread's authors and currentUser. | { emoji: string; userIds: string[] }[] |
Resolved thread
A resolved thread collapses to a summary row with Show thread and Reopen.
Resolved by Sam Okafor ยท 2 comments
SO Sam OkaforThis query runs once per row. Can we batch it?
RK Riya KapoorBatched with a single IN query, down from 120 calls to 1.
Read-only
Leave out currentUser to show the thread without a composer, actions or reaction toggles.
LC Lee ChenSigned off on the rollout plan. @priya owns the canary.
PN Priya NairCanary is at 5% and holding.
Controlled with async saves
Own the comments array and return a promise from onReply, onEdit or onDelete. The thread waits for it and keeps the draft if it rejects.
The empty state copy still says โprojectsโ. Should it be โworkspacesโ?
1 stored ยท Nothing saved yet