Chat Composer
The box a person talks to an agent in — chips for what travels with the message, the text with mentions, and Send, which turns into Stop.
- Status
- Since
0.43.0- Accessibility pattern
- named textbox, described by its hint and error
Last updated
Only you can see this chat
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Chat Composer before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify chat-composerInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/chat-composerRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
The same command installs the registry items it composes: @vegastack/button, @vegastack/text-edit, @vegastack/tooltip.
Usage
import { ChatComposer } from "@/components/ui/chat-composer";
<ChatComposer
placeholder="Ask anything, or @ an agent…"
status={status}
onSubmit={send}
onStop={stop}
hint="Only you can see this chat"
/>;The text is TextEdit, so a mention here is the same chip a comment
has. For a comment or a reply on a record, use
CommentComposer: this box is for a conversation, where Enter sends.
Only you can see this chat
Examples
While the agent answers
With status="streaming" the send button is Stop, and the person can keep typing their next
message. Leave out onStop for someone who may not stop this answer: Send stays, held.
Send held
submitDisabled holds Send and keeps the text, while a file uploads for example. The reason goes in
submitDisabledReason, a line under the box that also describes it.
datasheet.pdf is still uploading
A failed send
The box clears as soon as the person sends, so they can start the next message; resolve onSubmit
when the send is accepted, not when the answer ends. If it rejects, the text comes back, ahead of
anything typed since, with labels.sendFailed under the box. Pass error to say more. Resolving
false puts the text back with no error, for a question first: "Send without the skill?".
Couldn't send. Try again.
Turned off
disabled turns off the whole box, for an agent that is paused. The hint can say why.
BOQ processor is paused.
Filling it from outside
handleRef gives setValue, for a starter prompt that fills the box without sending, and focus.
setValue(draft, { focus: false }) applies a draft that arrived after the box mounted. The text is
Markdown both ways: onSubmit, onValueChange, defaultValue and setValue all carry it, so a
line break arrives as \\ and a new line.
const composer = useRef<ChatComposerHandle>(null);
<Button onClick={() => composer.current?.setValue("Plan my day")}>
Plan my day
</Button>;
<ChatComposer handleRef={composer} onSubmit={send} />;API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
autoFocus | boolean | false | Focus the box when it mounts. |
chips | React.ReactNode | — | What travels with the message, above the text: the page's chip, attached files. |
defaultValue | string | — | The starting text, as Markdown: a restored draft. |
disabled | boolean | false | Turn the whole box off. |
dropTypes | readonly string[] | — | dataTransfer types the box takes besides files: a record row dragged in. |
error | React.ReactNode | — | The message that failed to send, under the box; onSubmit's rejection shows otherwise. |
handleRef | React.Ref<ChatComposerHandle> | — | Reach into the box. |
hasAttachments | boolean | false | There is something to send besides the text, so an empty box may send. |
hint | React.ReactNode | — | The line under the box: who can see this chat. |
labels | ChatComposerLabels | — | The composer's words. |
mentionHref | ((kind: MentionKind, id: string) => string | null) | — | Where a mention chip links (see TextEdit's mentionHref). |
mentionImage | ((id: string) => string | null | undefined) | — | A person mention's avatar URL (see TextEdit's mentionImage). |
mentions | TextEditMentions | — | @ mentions (see TextEdit's mentions); also adds the @ button. |
mentionStatus | ((kind: MentionKind, id: string) => TextEditMentionStatus | null | undefined) | — | How a mention chip shows its target's state (see TextEdit's mentionStatus). |
onAttach | (() => void) | — | The 📎 button: open the host's uploads picker. Without it, there is no button. |
onDropData | ((data: DataTransfer) => void) | — | One of dropTypes was dropped. |
onEditLast | (() => void) | — | ↑ in the empty box: bring back the last message to edit. |
onFiles | ((files: File[]) => void) | — | Files pasted or dropped on the box. Without it, they go nowhere. |
onStop | (() => void) | — | Stop the answer. Without it, there is no Stop while streaming. |
onSubmit | ((value: string) => void | boolean | Promise<unknown>) | — | Send the message, as Markdown. The box clears at once, so the person can start the next one;
Send stays busy while a promise is pending, so it may resolve when the send is accepted, not
when the answer ends. A rejection, or resolving false (a question first: "Send without the
skill?"), puts the text back, ahead of anything typed since. Without it, Send stays off. |
onValueChange | ((value: string) => void) | — | Every change of the text, as Markdown, and "" once it sent. |
placeholder | string | — | Shown while the box is empty. |
status | ChatComposerStatus | "idle" | sending — the message is on its way. streaming — the agent is answering: the send button
becomes Stop and the person can keep typing their next message. |
submitDisabled | boolean | false | Hold Send only: something isn't ready (a file still uploading). The reason goes in
submitDisabledReason. |
submitDisabledReason | string | — | Why Send is held, in a line under the box: "datasheet.pdf is still uploading". |
Data attributes and CSS variables on ChatComposer
| Attribute | Values |
|---|---|
data-dragging | "" |
data-slot | "chat-composer" | "chat-composer-box" | "chat-composer-chips" | "chat-composer-drop" | "chat-composer-error" | "chat-composer-held" | "chat-composer-hint" | "chat-composer-send" | "chat-composer-stop" | "chat-composer-toolbar" |
data-status | mirrors a prop or state value |
| Prop | Type | Default | Description |
|---|---|---|---|
focus* | () => void | — | Put the caret in the box, at the end of the text. |
setValue* | (value: string, options?: { focus?: boolean; }) => void | — | Replace the text: a starter prompt, a message brought back to edit, a draft that arrived after
the box mounted. Focuses the box unless focus is false. |
| Prop | Type | Default | Description |
|---|---|---|---|
attach | string | "Add files" | |
drop | string | "Drop to add to chat" | |
mention | string | "Mention" | |
message | string | "Message" | The box's name. |
send | string | "Send" | |
sendFailed | string | "Couldn't send. Try again." | Under the box when a send fails, unless error says more. |
stop | string | "Stop" |
Accessibility
- The box is a textbox named by
labels.message, described by the hint and the error. - A failed send is
role="alert". It appears because of what the person did. - Enter sends everywhere but a code block, so in a list it sends too; Shift+Enter continues the line.
- Send, Stop, 📎 and @ are named buttons in the Tab order, with a tooltip each.
| Key | Action |
|---|---|
| Enter | Send; on a phone, or in a code block, a new line |
| Shift + Enter | A new line |
| ⌘ / Ctrl + Enter | Send, from anywhere in the text |
| ↑ | In the empty box, bring back the last message (onEditLast) |
| Esc | Leave the box; the text stays |
| Contract | States tested |
|---|---|
| Behaviour | idle, sending, streaming, stop-absent, empty, attachments-only, submit-held, send-failed, disabled, dragging, edit-last, filled-by-handle, mention-button |
| Accessibility | named-textbox, described-by-hint-and-error, named-buttons, alert-on-failure, browser-accessibility-test |
| Visual | empty, chips, streaming, held, error, disabled |
Do / Don't
Message Scroller
A chat transcript scroller that follows the live edge only while the reader is, anchors each new turn near the top, and never pulls them away.
Transcript
A timestamped, speaker-labelled transcript of a recording that follows playback, seeks from each line, and searches with highlighted matches.