import { TerminalComposer } from "@elabs-ai/components-terminal";npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-terminalPaste it into Claude Code, Cursor, Codex or any other coding agent. Text in [brackets] is yours to fill in.
Add the brand-ui TerminalComposer component (from @elabs-ai/components-terminal) to this project. 1. Read https://elabs-ai.com/llms.txt first: it maps every package and the rules. If you can use MCP servers, connect https://elabs-ai.com/mcp and use its search, docs and tokens tools. If not, run the same lookups in the terminal: `npx -y @elabs-ai/components-cli search "<what you need>"` and `npx -y @elabs-ai/components-cli docs <Component>`. 2. Look TerminalComposer up before writing code: `npx -y @elabs-ai/components-cli docs TerminalComposer` prints its real props, variants, what it pairs with and what to avoid. 3. If @elabs-ai/components-terminal is not installed yet: `pnpm add @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-terminal` (or `npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-terminal`). If this is the first brand-ui package in the project, wire it once: import `@elabs-ai/components-tokens/styles.css` plus the `themes/light.css` and `themes/dark.css` stylesheets in the CSS entry, add one Tailwind `@source` line per installed @elabs-ai package (Tailwind does not scan node_modules, so without it everything renders unstyled), and wrap the app root in `ThemeProvider` from `@elabs-ai/components-tokens`. 4. Use it here: [the screen or file, and what it should show] 5. Rules: use only @elabs-ai/components-* components and the semantic tokens — no raw colours, no raw font sizes, no arbitrary Tailwind values. Look up each component's real props with the docs tool before you use it; never guess a prop. 6. Done means: the typecheck passes, `npx -y @elabs-ai/components-cli audit src --strict` reports nothing, and you have opened the app and looked at every screen you touched in the light and the dark theme.
Avoid
- Shipping a vendor mode/effort union such as `"auto" | "accept-edits" | "plan"` in a public type — `modes`/`effortLevels` are entirely app-supplied `OperatingMode[]`/`EffortLevel[]` (#117 acceptance criterion).
- Using the native `disabled` attribute for the resting empty-composer state — it drops a focused control from the tab order after every keyboard submit; the affordance is `aria-disabled`, matching `PromptInputSubmit`'s contract.
- Adding an `onPaste` handler that calls `preventDefault()` — paste is never blocked, and the caret is the browser's own, tinted via `caret-terminal-cursor`, never a hand-drawn glyph.
- Relying on the effort scale's fill/size ramp alone — each rung also carries `aria-label={level.label}` and the current level's name renders as real visible text, so the level reaches assistive tech as words, not only as a filled shape.
- Importing `PromptInputMode`/`PromptInputEffort` or anything from `@elabs-ai/components-ai` — `terminal` and `ai` are layer-2 DAG siblings; the shared vocabulary (`OperatingMode`/`EffortLevel`/`effortRungForIndex`) lives in `@elabs-ai/components-ui` instead.
Works with
Examples
Type And Submit
Typing enables Send; submitting via Enter clears the (uncontrolled) well.
With Modes
An app-defined mode menu — glyph, label and key hint, no vendor vocabulary.
With Effort
The ordered effort scale — the filled/hollow squares are the greyscale channel; the current level's name is also real, visible text.
With Modes And Effort
Both indicators together, in the toolbar row below the well.
Busy
ADR 0022 case 2 (#128) — busy, this composer OWNS cancellation (`onStop` given), and the well is EMPTY: Send becomes Stop, Stop is never auto-disabled, and the shortcut row gains a cancel hint it loses again at rest. This is the composer-only arrangement, e.g. no `TerminalWorking` line rendered anywhere in the composition.
Busy With Follow Up
ADR 0022 case 3 (#128) — busy, but a follow-up is already typed: the control reverts to Send and the follow-up can be submitted mid-turn. This is the fix for the dead end where a running composer could never submit anything — case 3 wins over case 2 even though `onStop` is still given.
Busy No Dedicated Stop
ADR 0022 case 4 (#128) — busy, but this composer was NOT handed `onStop`: some other control (e.g. `TerminalWorking` in a real transcript) owns cancellation, so this control stays Send always and never advertises a cancel hint it couldn't honour. Fixes both the duplicate "Stop" control and the dead Stop button (`onStop?.()` on `undefined`).
Custom Shortcuts
A caller-supplied shortcut set entirely replaces the default hints.
Rail
`variant` forwards to the internal `TerminalSurface`, like every other component in this family.
Boxed
API
TerminalComposer
| Prop | Type | Description |
|---|---|---|
value | string | Composer text. Mirrors the native platform (`value`/`defaultValue`/ `onChange`) rather than a custom `onValueChange` — this IS a text input. Omit `value` for the uncontrolled default (`defaultValue ?? ""`). |
defaultValue | string | |
onChange | ChangeEventHandler<HTMLTextAreaElement> | |
onSubmit | (value: string) => void | Fires on Enter (no Shift, not composing) or the submit affordance, with the current text. Fires whenever there is non-whitespace text — INCLUDING while `busy` (ADR 0022 case 3, #128): a mid-turn follow-up can always be composed and submitted, exactly like `PromptInputTextarea`'s own contract. brand-ui asserts nothing about what a mid-turn submit MEANS (queued, interleaved, or something else) — that is the app's own `onSubmit`/runtime (D5). The caller decides whether/how to clear `value`; uncontrolled, the composer clears itself. |
placeholder | string | @default the localized "Type your next instruction…" |
busy | boolean | True while the agent is actively working on a previous turn. The submit affordance becomes Stop ONLY when the well is also empty AND `onStop` is given — i.e. this composer is the one meant to own cancellation (ADR 0022's four-case contract, #128). A non-empty well always shows Send, so a mid-turn follow-up can still be typed and submitted; `busy` with no `onStop` never shows Stop at all (some other control, e.g. `TerminalWorking`, owns cancellation instead). Enter-to-submit is never disabled by `busy` alone. |
onStop | () => void | Fires when the person activates the Stop affordance, or presses Escape, while `busy`. Passing this prop is also what tells the composer it OWNS cancellation — omit it when another control (e.g. `TerminalWorking`) is the dedicated Stop, so the two don't both render a control named "Stop" for the same action (#128). |
modes | OperatingMode[] | App-defined operating modes (#117 — no vendor union here). Omitted, no mode indicator renders. |
mode | string | |
defaultMode | string | |
onModeChange | (id: string) => void | |
effortLevels | EffortLevel[] | App-defined reasoning-effort scale, ordered low to high. Omitted, no effort indicator renders. |
effort | string | |
defaultEffort | string | |
onEffortChange | (id: string) => void | |
effortLabel | string | Accessible name for the effort scale. @default the localized "Effort" |
shortcuts | TerminalComposerShortcut[] | The hints shown at rest, e.g. `[{ keys: "Enter", label: "send" }]`. @default a localized Enter-to-send / Shift+Enter-for-newline pair. The cancel hint is added automatically while this composer can actually cancel the turn (`busy` AND `onStop` — see `busy`) — do not include it here; override it with `cancelShortcut` instead. |
cancelShortcut | TerminalComposerShortcut | Overrides the cancel hint shown while this composer owns cancellation (`busy` AND `onStop`, see `busy`). @default the localized "Esc" / "cancel" pair. |
variant | TerminalVariant | Gutter grammar forwarded to the internal `TerminalSurface`. |
textareaRef | Ref<HTMLTextAreaElement> | Ref to the underlying `<textarea>` DOM node. The `ref` this component already forwards reaches the root `TerminalSurface` div, not the field itself — a caller that needs caret math on every keystroke (a slash-command palette, an `@`-mention overlay) reaches for this instead. Additive and optional: every existing caller is unaffected. See `TerminalSlashMenu` (#117 T12) for the reference consumer. |
TerminalSurface
| Prop | Type | Description |
|---|---|---|
variant | TerminalVariant | Gutter grammar for every row inside. A row may override it per-row. |
gutter | string | Width of the gutter track, as any CSS length. Written to the local `--terminal-gutter` custom property — deliberately NOT a theme token: it does not vary by theme and no other package needs it. A caller's own `style` still wins over this. |
loading | boolean | `true` while the transcript has no renderable content yet (restoring a past session). Renders layout-shaped skeleton rows through the REAL `TerminalRow`, so the placeholder cannot drift from the grid it stands in for. Never a spinner. |
loadingRows | number | How many skeleton rows to reserve while `loading`. |
loadingLabel | string | Overrides the announced loading label. Defaults to the shared "Loading…". |
Theming
The tokens each state resolves to. Change the token and every theme follows.
| State | Token |
|---|---|
| effortFilled | border-terminal-accent! bg-terminal-accent |
| effortHollow | border-terminal-border! bg-transparent |
| modeTrigger | hover:bg-terminal-selection hover:text-terminal-foreground |
| submit | bg-terminal-accent text-terminal-accent-foreground |
| caret | caret-terminal-cursor |