KQ-1294 · THREAD/VIEW
Thread View
A complete assistant thread over a tree of messages: runs from one author share an avatar, each message's toolbar rises into place on snap, copy morphs into a check on flick and a reaction lands on recoil. Editing a sent message forks the thread, and the version switcher slides the message and every reply under it — the old branch out on the exit ease, the new one in on glide — while its counter rolls on snap and the edited bubble's measured height glides. Scrolled up, a pill counts the replies that land and glides the log to the newest on glide; End does the same, Enter sends or saves, Shift+Enter breaks a line and Escape cancels an edit.
Basin export retries
Fernworks Model 3
3 versions of the first turn · hover a message for its tools
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/thread-view
Props
- messages / defaultMessages
- ThreadMessage[]
- default defaultThreadMessages
- Every version of every turn as one flat list: messages that share a parentId are versions of one turn. Controlled or uncontrolled.
- onMessagesChange
- (messages: ThreadMessage[]) => void
- default —
- Fires from the send, edit or reaction that changed the list, with the whole new list.
- leaf / defaultLeaf
- string
- default —
- Any message on the branch to show. The thread runs from the first turn through it, then on down the newest replies. Defaults to the newest version of every turn.
- onLeafChange
- (leaf: string) => void
- default —
- Fires when a version switch, a send or an edit moves the visible branch.
- density
- "compact" | "cozy" | "roomy"
- default "cozy"
- How tightly the thread is set: type size, bubble padding, avatar size and the gap between runs.
- branches
- boolean
- default true
- Edits keep every version, with a switcher to move between them. Off: the data still forks, but an edit reads as a replacement.
- toolbar
- "hover" | "always" | "off"
- default "hover"
- Each message's actions: a pill on hover, focus or tap, a row always shown, or none for a read-only transcript.
- onSend
- (text: string, message: ThreadMessage) => void
- default —
- A message was sent from the composer. It is already in the list; stream the reply in as a child of message.id.
- onEdit
- (original: ThreadMessage, version: ThreadMessage) => void
- default —
- A message was edited: version is the new sibling of original and the branch now shows it.
- onCopy
- (text: string, messageId: string) => void
- default —
- A message or one of its code blocks was copied to the clipboard.
- onReact
- (messageId: string, reaction: "up" | "down" | null) => void
- default —
- A reply was marked good or bad, or the mark was cleared.
- onRetry
- (messageId: string) => void
- default —
- Retry was pressed on a reply whose status is "error".
- onReload
- () => void
- default —
- Try again was pressed while status is "error".
- now
- number | Date
- default —
- The moment relative times count from. Defaults to the newest message's time, so the thread renders the same on the server and in the browser.
- author
- string
- default —
- The name new messages are sent under. Defaults to the newest user message's author, else "You".
- title
- string
- default "Basin export retries"
- The thread's name in the header; also its accessible name unless label is given.
- model
- string
- default "Fernworks Model 3"
- The model answering, shown in the header and the composer's placeholder.
- status
- "ready" | "loading" | "error"
- default "ready"
- Whether the thread's data has arrived: a skeleton while loading, a message with Try again on error.
- composer
- boolean
- default true
- Show the composer: a textarea that grows to five lines, Enter to send, Shift+Enter for a new line.
- placeholder
- string
- default —
- The composer's placeholder. Defaults to "Message" and the model's name.
- suggestions
- string[]
- default —
- Prompts offered while the thread is empty; pressing one fills the composer.
- label
- string
- default —
- The thread's accessible name. Defaults to the title.
- sound
- boolean
- default false
- A pop for a send, a saved edit or a reaction; a tick for a version switch, a copy and the jump to the latest.
- disabled
- boolean
- default false
- Read only: nothing can be sent, edited or reacted to.
- className
- string
- default —
- Classes for the root. It is 560px tall by default; pass a height class to change it.
| Prop | Type | Default | Description |
|---|---|---|---|
| messages / defaultMessages | ThreadMessage[] | defaultThreadMessages | Every version of every turn as one flat list: messages that share a parentId are versions of one turn. Controlled or uncontrolled. |
| onMessagesChange | (messages: ThreadMessage[]) => void | — | Fires from the send, edit or reaction that changed the list, with the whole new list. |
| leaf / defaultLeaf | string | — | Any message on the branch to show. The thread runs from the first turn through it, then on down the newest replies. Defaults to the newest version of every turn. |
| onLeafChange | (leaf: string) => void | — | Fires when a version switch, a send or an edit moves the visible branch. |
| density | "compact" | "cozy" | "roomy" | "cozy" | How tightly the thread is set: type size, bubble padding, avatar size and the gap between runs. |
| branches | boolean | true | Edits keep every version, with a switcher to move between them. Off: the data still forks, but an edit reads as a replacement. |
| toolbar | "hover" | "always" | "off" | "hover" | Each message's actions: a pill on hover, focus or tap, a row always shown, or none for a read-only transcript. |
| onSend | (text: string, message: ThreadMessage) => void | — | A message was sent from the composer. It is already in the list; stream the reply in as a child of message.id. |
| onEdit | (original: ThreadMessage, version: ThreadMessage) => void | — | A message was edited: version is the new sibling of original and the branch now shows it. |
| onCopy | (text: string, messageId: string) => void | — | A message or one of its code blocks was copied to the clipboard. |
| onReact | (messageId: string, reaction: "up" | "down" | null) => void | — | A reply was marked good or bad, or the mark was cleared. |
| onRetry | (messageId: string) => void | — | Retry was pressed on a reply whose status is "error". |
| onReload | () => void | — | Try again was pressed while status is "error". |
| now | number | Date | — | The moment relative times count from. Defaults to the newest message's time, so the thread renders the same on the server and in the browser. |
| author | string | — | The name new messages are sent under. Defaults to the newest user message's author, else "You". |
| title | string | "Basin export retries" | The thread's name in the header; also its accessible name unless label is given. |
| model | string | "Fernworks Model 3" | The model answering, shown in the header and the composer's placeholder. |
| status | "ready" | "loading" | "error" | "ready" | Whether the thread's data has arrived: a skeleton while loading, a message with Try again on error. |
| composer | boolean | true | Show the composer: a textarea that grows to five lines, Enter to send, Shift+Enter for a new line. |
| placeholder | string | — | The composer's placeholder. Defaults to "Message" and the model's name. |
| suggestions | string[] | — | Prompts offered while the thread is empty; pressing one fills the composer. |
| label | string | — | The thread's accessible name. Defaults to the title. |
| sound | boolean | false | A pop for a send, a saved edit or a reaction; a tick for a version switch, a copy and the jump to the latest. |
| disabled | boolean | false | Read only: nothing can be sent, edited or reacted to. |
| className | string | — | Classes for the root. It is 560px tall by default; pass a height class to change it. |