src/payments/retry.ts ยท line 42

  1. NH
    Noor Haddad

    Should this retry on 429 as well? The gateway rate-limits bursts after a deploy. cc @tomasz

    1. TW
      Tomasz Wierzba

      Yes, but only with the Retry-After header. Otherwise we hammer it again.

    2. 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.

comment-thread-usage.tsx
"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
commentsComment[]Controlled comments. Top-level comments render in array order, each followed by its replies in array order.
defaultCommentsComment[]Default[]Initial comments when uncontrolled.
onCommentsChange(comments: Comment[]) => voidCalled with the full list after a comment is posted, edited or deleted, or a reaction changes.
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.
mentionSuggestionsMentionOption[]Default[]People offered after @ in the composer. An @value in a body is highlighted only when value is in this list.
reactionsReactionOption[]DefaultQUICK_REACTIONSEmoji offered in the reaction picker, each { emoji, label }. The default is ๐Ÿ‘ ๐Ÿ‘Ž ๐Ÿ˜„ ๐ŸŽ‰ ๐Ÿ˜• โค๏ธ ๐Ÿš€ ๐Ÿ‘€.
resolvedbooleanControlled resolved state. A resolved thread collapses to a summary row with Show thread, plus Reopen when currentUser is set.
defaultResolvedbooleanDefaultfalseInitial resolved state when uncontrolled.
onResolvedChange(resolved: boolean) => voidCalled when someone resolves or reopens the thread.
resolvedByCommentAuthorShown as Resolved by name. Falls back to currentUser when the thread was resolved in this session.
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.
onEdit(id: string, body: string, mentions: string[]) => void | Promise<void>Called when an edit is saved with different text. Same promise handling as onReply.
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.
onReactionToggle(id: string, emoji: string) => voidCalled after the current user adds or removes a reaction.
nowDateReference time for relative timestamps. Without it the thread uses the current time and updates every minute.
localestringLocale for relative and absolute timestamps. Defaults to the browser locale.
aria-labelstringDefault"Comment thread"Accessible name of the thread region.

Other props go to the <section>.

Comment

One comment or reply.

Prop
idRequiredstringUnique id.
parentIdstring | nullThe comment this replies to. Missing parents make it a top-level comment.
authorRequiredCommentAuthor{ id: string; name: string; avatarUrl?: string }. Initials show when there is no avatarUrl or it fails to load.
bodyRequiredstringPlain text. Line breaks are kept and HTML is shown as text.
createdAtRequiredDate | stringAny value new Date() accepts. An invalid date hides the timestamp.
editedAtDate | stringShows (edited) with the time in its tooltip.
deletedbooleanShows a deleted placeholder while the comment has replies, and hides it otherwise.
reactions{ emoji: string; userIds: string[] }[]Each emoji and who reacted. Names come from the thread's authors and currentUser.

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.

  1. AL
    Ana Lima

    The empty state copy still says โ€œprojectsโ€. Should it be โ€œworkspacesโ€?

1 stored ยท Nothing saved yet

Resolved thread

A resolved thread collapses to a summary row with Show thread and Reopen.

Resolved by Sam Okafor ยท 2 comments

Read-only

Leave out currentUser to show the thread without a composer, actions or reaction toggles.

  1. LC
    Lee Chen

    Signed off on the rollout plan. @priya owns the canary.

    1. PN
      Priya Nair

      Canary is at 5% and holding.

components/ui/comment-thread.tsx
CtrlK