Pathfinderby Commenda
AI elements

Composer

A controlled AI draft editor with skills, attachments, context, and send or stop actions.

Usage

Example.tsx
import { Composer } from '@commenda/pathfinder/composer';

<Composer
  label="Message"
  value={draft}
  onValueChange={setDraft}
  onSubmit={({ text, attachments, command }) => {
    // Set request status synchronously; catch failures in your request handler.
    sendDraft({ text, attachments, command });
  }}
  requestConfig={{ status, onStop: stop }}
/>;

The host owns the draft, uploads, selected skill, and request lifecycle. Composer never starts a network request or clears data. Clear the submitted draft only after acceptance, and avoid clearing text the user has typed since submitting. Handle rejected requests in the application and preserve the draft for retry.

Standard and hero

Standard starts with one 20px text line. Hero reserves a 53px editing area. Both grow with content, cap the composer surface at 40dvh, and keep Send reachable while the editor scrolls. The maximum width is 736px; smaller containers fill available width.

Type / at a word boundary to open skills. Text after the slash filters local labels and descriptions. Arrow keys move through enabled results; Enter selects without sending. Selection removes the query and displays the selected skill as a blue Tag beside the editable text. The submit payload preserves the command separately from plain text; no rich-text document or Vercel message object is required.

Example.tsx
<Composer
  label="Message"
  value={draft}
  onValueChange={setDraft}
  onSubmit={submit}
  commandConfig={{
    items: results,
    selected: selectedSkill,
    onSelect: setSelectedSkill,
    filterMode: 'manual',
    onSearchChange: setQuery,
    isLoading,
  }}
/>;

Manual mode preserves API ranking and does not filter results again. onSearchChange receives the query while open and null when closed. Debounce, cancel outdated requests, and supply results only for the current query. Set isLoading while results are stale: selection is blocked and previous options are hidden. Empty results cannot accidentally send the query. The following example simulates an API; search for tax to receive semantically related results.

The selected skill is one token preceding the plain-text draft. This is not an arbitrary inline rich-text mention editor. Shared context comes from the host through contextConfig; no undocumented @ picker is introduced.

Attachments, mode, and dictation

attachmentConfig composes AttachmentGroup above the editor. onAdd opens your existing file picker or upload menu; validate with AttachmentValidation before uploading. Pending or failed uploads disable Send until resolved or removed. Removal, retry, previews, cancellation, and URLs remain application-owned.

toolbar.leading accepts Mode/Model Select or Dropdown controls. toolbar.trailing accepts a dictation IconButton. Render only actions your application supports and pass their disabled states explicitly. Composer does not request microphone permission or connect to a speech service.

Agent

Context

Pass ComposerContext's top or bottom configuration through contextConfig. Context stays separate from the outgoing text; your host adds any domain identifiers to its request. To dismiss a bottom invitation, retain its position and set connectorConfig: null.

Acme Pte LtdQ3 GST filing

Sending and failure

submitted and streaming show Stop. Typing remains available, but Enter never queues a message. ready and error allow an explicit retry when text and attachments are valid. An absent onStop disables Stop. Errors belong in the appropriate Attachment, Message, or host notification, with no red composer border.

Keyboard and accessibility

  • Provide a meaningful label; the placeholder is not the accessible name.
  • enter sends with Enter, while Shift+Enter inserts a newline. mod-enter sends with Command/Ctrl+Enter and leaves ordinary Enter for newlines.
  • IME composition does not submit or choose a skill. Pasted line breaks are preserved.
  • Skills use a listbox linked to the textarea, with an active descendant. Focus stays in the editor.
  • Escape closes suggestions and stops propagation. It never cancels generation; use Stop. Tab leaves suggestions without choosing a result.
  • Disabled state locks the editor and built-in actions. Its visible reason is associated with the editor. Host toolbar controls must also receive disabled state.
  • The forwarded ref is the textarea, so selecting a starter prompt can focus it.

This conversation is archived.

Mobile layout

The host uses a flex column with scrollable conversation history and a footer that stays above the software keyboard. This example listens to visualViewport.resize, bounds its height, and includes the bottom safe area. Composer stays in normal document flow. Test the containing application on actual iOS/Android keyboards before release.

Conversation history scrolls here. Focus the editor on a phone to keep it above the keyboard.

API reference

PropTypeBehavior
labelRequiredstringRequired accessible editor name
valueRequiredstringRequired controlled draft
onValueChangeRequired(value: string) => voidRequired controlled draft
onSubmitRequired( message: ComposerSubmission ) => voidRequired; host handles async failures
size"standard"|"hero"standard
placeholderstringAsk Commenda anything; mention / only when skills are configured
requestConfigObjectView fieldsstatus: ready, submitted, streaming, or error
attachmentConfigOmit< AttachmentGroupProps, 'className' | 'align' > & { onAdd?: () => void; addLabel?: string; }Alignment/class controlled internally; default three rows
commandConfigComposerCommandConfigView fieldsOptional local/manual skill list and controlled selection
contextConfigComposerContextConfigView fields · 2 shapesTop context or bottom connector invitation
toolbarObjectView fieldsApplication-owned controls
submitShortcut"enter"|"mod-enter"enter
disableConfigObjectView fieldsOptional disabled state and caption
inputProps
View full typePick<TextareaHTMLAttributes<HTMLTextAreaElement>, "autoFocus" | "id" | "spellCheck" | "aria-describedby" | "onFocus" | "onBlur" | "name" | "autoComplete" | "maxLength">
id, name, autoFocus, autoComplete, spellCheck, maxLength, aria-describedby, onFocus, onBlur
classNamestringOuter layout and imperative focus
requestConfigObject fields
FieldTypeRequirement
status"ready" | "submitted" | "streaming" | "error"Required
onStop(() => void)Optional
submitLabelstringOptional
stopLabelstringOptional
commandConfigObject fields
FieldTypeRequirement
itemsreadonly ComposerCommand[]Required
selectedObjectRequired
onSelect(command: ComposerCommand | null) => voidRequired
filterMode"local" | "manual"Optional

Manual mode accepts results as supplied, without applying a second filter.

onSearchChange((query: string | null) => void)Optional
isLoadingbooleanOptional
labelstringOptional
emptyLabelstringOptional
commandConfig.selectedObject fields
FieldTypeRequirement
idstringRequired
labelstringRequired
descriptionstringOptional
disabledbooleanOptional
contextConfigObject fields

Choose one of these 2 shapes. Do not combine fields excluded by the selected shape.

Option 1

FieldTypeRequirement
itemsreadonly ComposerContextItem[]Required
onRemove((id: string) => void)Optional
position"top"Optional

Do not supply: connectorConfig.

Option 2

FieldTypeRequirement
position"bottom"Required
connectorConfigObjectRequired

Do not supply: items, onRemove.

contextConfig.connectorConfigObject fields
FieldTypeRequirement
labelstringRequired
logosreadonly ReactNode[]Optional
onConnect() => voidRequired
onDismiss(() => void)Optional
toolbarObject fields
FieldTypeRequirement
leadingReactNodeOptional
trailingReactNodeOptional
disableConfigObject fields
FieldTypeRequirement
isDisabledbooleanRequired
reasonstringOptional

ComposerSubmission contains { text: string; attachments: readonly AttachmentItem[]; command: ComposerCommand | null }. ComposerCommand contains { id, label, description?, disabled? }. IDs must be unique and stable. ComposerCommandConfig requires items, selected, and onSelect, with optional filterMode, onSearchChange, isLoading, label, and emptyLabel.

Design rules

  • COMPOSER-01: Figma 580:28873: 16px inset, 6px gap, 20px radius, pearl400 hairline, soft raised shadow, 736px maximum. Hover/focus border uses blue700.
  • COMPOSER-02: Send requires nonblank text and ready attachments. Generating changes Send to Stop without locking the draft.
  • COMPOSER-03: Never clear, silently queue, upload, or execute commands internally.
  • COMPOSER-04: Respect IME and both keyboard modes. Escape is scoped to suggestions; explicit Stop avoids cancelling a request when dismissing a menu.
  • COMPOSER-05: Do not render a red error composer. Disabled state explains why in a caption.
  • COMPOSER-06: Skills use a searchable listbox and blue Tag; toolbar actions use existing Pathfinder controls. Application context is supplied by the host.
  • COMPOSER-07: Mobile editor text is 16px. The host docks its chat layout above the keyboard and accounts for safe areas; Composer itself does not take over the viewport.

The existing TextArea's field shell is intentionally not reused: it adds a second border, field padding, and an 80px minimum. Composer keeps a private native textarea and reuses AttachmentGroup, ComposerContext, Tag, and IconButton.

On this page