import { TerminalSlashMenu } 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 TerminalSlashMenu 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 TerminalSlashMenu up before writing code: `npx -y @elabs-ai/components-cli docs TerminalSlashMenu` 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
- Re-declaring `SlashCommand`/`defaultSlashCommandFilter`/`stepIndex`/`findTriggerQuery`/`replaceTriggerRun` locally — all five are promoted to `@elabs-ai/components-ui` precisely so this package and `@elabs-ai/components-ai` cannot drift; import them.
- Detecting the `/` trigger from a keydown handler instead of deriving it from committed text + caret — a keydown-driven trigger has to `preventDefault()` every printable character, which breaks IME composition, undo, spellcheck and paste.
- Calling `findTriggerQuery` with the default word boundary — a slash palette needs `{ boundary: "line-start" }`, or `cd /usr` would wrongly open the popup mid-command.
- Distinguishing the active row with `bg-terminal-selection` alone — a sighted user in greyscale needs a second channel too, so a reserved-width `❯` marker (this family's established 'current' glyph) renders only on the active row.
- Querying the textarea with `querySelector` — `TerminalComposer`'s `textareaRef` prop exists precisely so a caret-tracking wrapper never needs to.
- Leaving a row's description at `text-terminal-muted` once it is active — `--terminal-selection` is only authored to guarantee AA for `--terminal-foreground`/`--terminal-ansi-white` (see that token's comment in `themes.css`), and `--terminal-muted` measures 3.61:1 on it, a real axe failure a unit test cannot see. The active row upgrades to `text-terminal-foreground`.
TerminalSlashMenu — the console composer's /-command palette. Typing / at the START of a line (never mid-word, never mid-path — cd /usr does not open it) opens a filtered listbox of app-defined commands anchored to the composer's own textarea; arrow keys move the highlight (wrapping at both ends, clamping into a narrowed list), Enter splices the command in, Escape dismisses — focus never leaves the field. Ships no command vocabulary of its own; commands is entirely prop-driven.
Works with
Examples
Palette Open
Typing `/` at the start of the line opens the palette; arrow keys move the highlight without ever moving focus off the textarea.
Not Mid Word
Typing `/` mid-word (not at the start of a line) never opens the palette — the acceptance test for the line-start boundary this palette uses instead of `MentionInput`'s word boundary.
Narrowing Clamps The Highlight
Narrowing the query clamps the highlighted option into the filtered list — it can never point at an option that has scrolled out of the results.
Select With Enter
Enter inserts the highlighted command and closes the palette.
Close With Escape
Escape closes the palette and returns focus to the textarea, selecting nothing.
No Matches
No commands match the typed query — a real empty state renders as a sibling of the listbox (never a collapsed popover with nothing in it).
API
TerminalSlashMenu
| Prop | Type | Description |
|---|---|---|
commandsrequired | SlashCommand[] | The registered commands the palette offers. No vendor command set is shipped. |
valuerequired | string | The composer's current text. Always controlled — see the module doc for why. |
onValueChangerequired | (next: { text: string; caret: number }) => void | The text changed — by typing, or by selecting a command (which splices `"/" + command.name + " "` over the typed query and reports the caret that follows it, the same `{ text, caret }` shape `replaceTriggerRun` returns). |
filter | (command: SlashCommand, query: string) => boolean | Which commands survive the current query. @default defaultSlashCommandFilter |
onSelectCommand | (command: SlashCommand) => void | A command was chosen (fires alongside, not instead of, `onValueChange`). |
onOpenChange | (open: boolean) => void | The popup opened or closed. |
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. |
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. |
Theming
The tokens each state resolves to. Change the token and every theme follows.
| State | Token |
|---|---|
| content | bg-terminal-background text-terminal-foreground |
| activeItem | bg-terminal-selection |
| activeMarker | text-terminal-accent |