# Composer

Where a person asks. Mentions (@) attach things in the user's system as chips the agent receives as references, not as text it has to parse; slash commands (/) start a known action. The scope pill says which environment the ask runs against, and turns to a warning when that is production.

Enter sends, Shift+Enter breaks a line. While an agent replies the send button becomes Stop. A locked composer says why; an offline one says so. With `onAttach`, files can also be pasted or dropped onto it. `tools` adds your own controls to the toolbar, such as a dictation button. A held composer (`{ kind: "held", reason }`) keeps typing open and says why sending waits. `textareaRef` focuses the box from outside, e.g. after a new project starts.

- Registry name: `composer` (`@candor/composer`)
- Tier: Free (MIT). No licence key needed.
- Docs: https://candor.themethodic.com/docs/composer

## Install

```bash
npx candor-ui add composer
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/composer`.
It brings in: attachment-preview, chat, entity-chip, locale, tooltip, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { Composer } from "@/components/composer"
import type { Attachment, EntityRef } from "@/lib/chat"

export default function Example() {
  const [value, setValue] = React.useState("")
  const [refs, setRefs] = React.useState<EntityRef[]>([])
  // Attach with the button, by pasting a screenshot, or by dropping files on the box. A real app uploads here and
  // sets progress, then a previewUrl it serves; this one only lists them.
  const [files, setFiles] = React.useState<Attachment[]>([{ id: "f0", name: "refund-policy.pdf", size: 182_000, mediaType: "application/pdf" }])
  const attach = (picked: File[]) =>
    setFiles((f) => [...f, ...picked.map((p, i) => ({ id: `f${f.length + i + 1}`, name: p.name, size: p.size, mediaType: p.type }))])
  return (
    <Composer
      className="max-w-2xl"
      value={value}
      onChange={setValue}
      onSubmit={() => setValue("")}
      references={refs}
      onReferencesChange={setRefs}
      attachments={files}
      onAttach={attach}
      onRemoveAttachment={(id) => setFiles((f) => f.filter((x) => x.id !== id))}
      scope={{ environment: "Shop", detail: "staging", production: false }}
      mentions={[
        { id: "m1", group: "Tables", label: "orders", entity: { kind: "table", name: "orders" }, description: "Every order" },
        { id: "m2", group: "People", label: "Dana Ortiz", description: "Support lead" },
      ]}
      commands={[{ id: "c1", name: "refund", description: "Draft a refund for an order", args: "<order>" }]}
    />
  )
}
```

## <Composer>

Props:

- `value: string`: The text in the box.
- `onChange: (value: string) => void`: Called with the new text as the person types.
- `onSubmit: (value: string, references: EntityRef[]) => void`: Called on send with the text and the entities mentioned.
- `onStop?: (() => void) | undefined`: Shown as Stop while a reply is streaming.
- `state?: ComposerState | undefined` (default `"ready"`): ready, sending, streaming, locked (with a reason), held (typing open, sending waits, with a reason) or offline.
- `scope?: ComposerScope | undefined`: Where the next message's work lands; production turns the pill to a warning.
- `onScopeClick?: (() => void) | undefined`: Makes the scope pill a button, e.g. to change environment.
- `mentions?: MentionOption[] | undefined` (default `[]`): What @ can insert, grouped under their headings.
- `commands?: SlashCommand[] | undefined` (default `[]`): What / can run, only at the start of a message.
- `references?: EntityRef[] | undefined` (default `[]`): Entities already attached, shown as chips. A chip added after the composer drew pops in; a removed one pops out.
- `onReferencesChange?: ((references: EntityRef[]) => void) | undefined`: Called when a chip is added or removed.
- `attachments?: Attachment[] | undefined` (default `[]`): Files attached, with upload progress or an error, as chips that pop in when added and out when removed.
- `onAttach?: ((files: File[]) => void) | undefined`: Shows an attach button; called with the chosen files.
- `accept?: string | undefined`: Which files the attach button's picker offers, as for `<input accept>` (e.g. "image/*,.pdf"). Files pasted or dropped arrive as they are, so check types where you upload.
- `onRemoveAttachment?: ((id: string) => void) | undefined`: Called when a file is removed.
- `placeholder?: string | undefined`: Shown in the empty box; "Ask a question, or describe a change" by default.
- `label?: string | undefined`: The accessible name of the text box; "Message" by default.
- `tools?: React.ReactNode | undefined`: Your own controls in the toolbar, after the attach button: a dictation button, a model picker.
- `hints?: boolean | undefined` (default `true`): The keyboard hints shown at wide widths ("↵ send · ⇧↵ new line"), naming @ and / only when `mentions` or `commands` are given. Defaults to true; false hides them.
- `textareaRef?: React.Ref<HTMLTextAreaElement> | undefined`: The text box, to focus it or read its selection from outside.
- `sendShape?: "square" | "round" | undefined` (default `"square"`): The send button's shape: "square" (the default, rounded corners) or "round".
- `className?: string | undefined`

## Strings (`composer`)

The fixed words, English by default. Override any of them under `composer` in `<LocaleProvider strings={{ composer: { … } }}>`; the table is exported as `composerStrings`.

- `placeholder`: Ask a question, or describe a change
- `label`: Message
- `commands`: Commands
- `mentions`: Mentions
- `attachFiles`: Attach files
- `production`: Production
- `nonProduction`: Non-production
- `offline`: You’re offline. Your message will wait here.
- `hintSend`: send
- `hintNewLine`: new line
- `hintMention`: mention
- `hintCommand`: command
- `stop`: Stop
- `send`: Send
- `dropFiles`: Drop files to attach

## Types

### ComposerState

```ts
| "ready" | "sending" | "streaming" | { kind: "locked"; reason: string } | { kind: "offline"; reason?: string | undefined } /** Typing stays open, but sending waits, and the box says why: "Your partner is still thinking". */ | { kind: "held"; reason: string }
```

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
