Skip to main content

TerminalComposer

Inputterminal

The prompt composer for the agent-session family: a text input well, an optional mode indicator, an optional ordered effort scale, a shortcut-hint row, and a submit affordance that becomes a stop affordance while busy.

import { TerminalComposer } from "@elabs-ai/components-terminal";
Install
npm install @elabs-ai/components-tokens @elabs-ai/components-ui @elabs-ai/components-terminal

Skipping the @source line is the most common first failure: Tailwind does not scan node_modules, so every component renders unstyled. The one-time wiring

Prompt for your coding agent

Paste 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

Pairs with

Avoid next to

  • PromptInput (that is the chat skin of the same composer concept, not a console-transcript sibling)

Examples

11 live 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

PropTypeDescription
valuestringComposer 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 ?? ""`).
defaultValuestring
onChangeChangeEventHandler<HTMLTextAreaElement>
onSubmit(value: string) => voidFires 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.
placeholderstring@default the localized "Type your next instruction…"
busybooleanTrue 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() => voidFires 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).
modesOperatingMode[]App-defined operating modes (#117 — no vendor union here). Omitted, no mode indicator renders.
modestring
defaultModestring
onModeChange(id: string) => void
effortLevelsEffortLevel[]App-defined reasoning-effort scale, ordered low to high. Omitted, no effort indicator renders.
effortstring
defaultEffortstring
onEffortChange(id: string) => void
effortLabelstringAccessible name for the effort scale. @default the localized "Effort"
shortcutsTerminalComposerShortcut[]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.
cancelShortcutTerminalComposerShortcutOverrides the cancel hint shown while this composer owns cancellation (`busy` AND `onStop`, see `busy`). @default the localized "Esc" / "cancel" pair.
variantTerminalVariantGutter grammar forwarded to the internal `TerminalSurface`.
textareaRefRef<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.

Also accepts: Omit< HTMLAttributes<HTMLDivElement>, "onChange" | "onSubmit" >

TerminalSurface

PropTypeDescription
variantTerminalVariantGutter grammar for every row inside. A row may override it per-row.
gutterstringWidth 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.
loadingboolean`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.
loadingRowsnumberHow many skeleton rows to reserve while `loading`.
loadingLabelstringOverrides the announced loading label. Defaults to the shared "Loading…".

Also accepts: HTMLAttributes<HTMLDivElement>

Theming

The tokens each state resolves to. Change the token and every theme follows.

StateToken
effortFilledborder-terminal-accent! bg-terminal-accent
effortHollowborder-terminal-border! bg-transparent
modeTriggerhover:bg-terminal-selection hover:text-terminal-foreground
submitbg-terminal-accent text-terminal-accent-foreground
caretcaret-terminal-cursor