Skip to content
Component installs need the registry setup
VegaStack Design

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
stable
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-composer

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/chat-composer

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

Compare these two datasheets

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?".

Plan my day

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

PropTypeDefaultDescription
autoFocusbooleanfalseFocus the box when it mounts.
chipsReact.ReactNode—What travels with the message, above the text: the page's chip, attached files.
defaultValuestring—The starting text, as Markdown: a restored draft.
disabledbooleanfalseTurn the whole box off.
dropTypesreadonly string[]—dataTransfer types the box takes besides files: a record row dragged in.
errorReact.ReactNode—The message that failed to send, under the box; onSubmit's rejection shows otherwise.
handleRefReact.Ref<ChatComposerHandle>—Reach into the box.
hasAttachmentsbooleanfalseThere is something to send besides the text, so an empty box may send.
hintReact.ReactNode—The line under the box: who can see this chat.
labelsChatComposerLabels—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).
mentionsTextEditMentions—@ 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.
placeholderstring—Shown while the box is empty.
statusChatComposerStatus"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.
submitDisabledbooleanfalseHold Send only: something isn't ready (a file still uploading). The reason goes in submitDisabledReason.
submitDisabledReasonstring—Why Send is held, in a line under the box: "datasheet.pdf is still uploading".

Data attributes and CSS variables on ChatComposer

AttributeValues
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-statusmirrors a prop or state value
PropTypeDefaultDescription
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.
PropTypeDefaultDescription
attachstring"Add files"
dropstring"Drop to add to chat"
mentionstring"Mention"
messagestring"Message"The box's name.
sendstring"Send"
sendFailedstring"Couldn't send. Try again."Under the box when a send fails, unless error says more.
stopstring"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.
KeyAction
EnterSend; on a phone, or in a code block, a new line
Shift + EnterA new line
⌘ / Ctrl + EnterSend, from anywhere in the text
↑In the empty box, bring back the last message (onEditLast)
EscLeave the box; the text stays
ContractStates tested
Behaviouridle, sending, streaming, stop-absent, empty, attachments-only, submit-held, send-failed, disabled, dragging, edit-last, filled-by-handle, mention-button
Accessibilitynamed-textbox, described-by-hint-and-error, named-buttons, alert-on-failure, browser-accessibility-test
Visualempty, chips, streaming, held, error, disabled

Do / Don't

Do
Say who can see the chat in the hint, and keep the person's text when anything fails.
Don't
Use it for comments on a record, where Enter must not post half a thought.

On this page