Skip to main content

TerminalSlashMenu

Inputterminal

The /-command palette for the console composer: a popover listbox anchored to TerminalComposer's own textarea, filtered by prefix, navigated with wrapping/clamped arrow keys, and spliced into the text on Enter — the caret never leaves the field.

import { TerminalSlashMenu } 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 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

Avoid next to

  • PromptInputSlash (that is the chat skin of the same trigger-palette concept, not a console-transcript sibling)

Examples

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

PropTypeDescription
commandsrequiredSlashCommand[]The registered commands the palette offers. No vendor command set is shipped.
valuerequiredstringThe composer's current text. Always controlled — see the module doc for why.
onValueChangerequired(next: { text: string; caret: number }) => voidThe 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) => booleanWhich commands survive the current query. @default defaultSlashCommandFilter
onSelectCommand(command: SlashCommand) => voidA command was chosen (fires alongside, not instead of, `onValueChange`).
onOpenChange(open: boolean) => voidThe popup opened or closed.
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< TerminalComposerProps, "value" | "defaultValue" | "onChange" >

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" >

Theming

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

StateToken
contentbg-terminal-background text-terminal-foreground
activeItembg-terminal-selection
activeMarkertext-terminal-accent