# @elabs-ai/components-terminal
> Terminal surfaces: shell/agent output and coding-agent CLI look-alikes.
Import from `@elabs-ai/components-terminal`. Part of brand-ui — see the root `llms.txt` for routing.
## Components
- DEFAULT_TERMINAL_GUTTER
- InteractiveTerminal — Streaming terminal surface for agent shell output, with an optional input line.
- avoid: Spawning a shell or process from the component — it renders a stream; the app owns the runtime (D5).
- avoid: Looking for an isStreaming/output prop — this surface is imperative: hold the InteractiveTerminalHandle ref and call write() per chunk (the declarative sibling is Terminal).
- avoid: Reaching the terminal engine through a static import — heavy engines must be reached by a dynamic import() (the heavy-deps ratchet).
- Terminal — Read-only ANSI console output with copy/clear actions and stick-to-bottom streaming.
- avoid: Reaching for it when the user must TYPE — that is InteractiveTerminal; Terminal is output-only.
- avoid: Stripping the ANSI codes before passing them — the component renders them; pre-stripping throws away the colour.
- avoid: Re-implementing auto-scroll in the consumer — `autoScroll` is owned by the component and must yield when the user scrolls up.
- TERMINAL_TOOL_CALL_STATUSES
- TERMINAL_TRANSCRIPT_ROW_KINDS
- TERMINAL_VARIANTS
- TERMINAL_WORKING_ACTIVE_GLYPH
- TERMINAL_WORKING_SPINNER_FRAMES
- TerminalActions
- TerminalBanner — The console-dress launch card above an empty transcript: identity (name, model, version, workspace), capabilities, what's new and quick actions with key hints — every section independently optional.
- avoid: Using `
`/`` to sit the title in the border — that misreports a decorative heading as a form-control group to assistive tech; it renders a real Heading beside a real border instead.
- avoid: Rendering a vendor bitmap logo — the component ships no mark of its own; `logo` is a caller-supplied `ReactNode` slot, never a baked-in wordmark (#117 acceptance criterion).
- avoid: Truncating `workspace` to one line — long paths WRAP so the full value stays reachable, matching the family's wrap-over-truncate fidelity axis rather than SessionHeader's single-line truncation.
- avoid: Importing `SessionHeader` or anything from `@elabs-ai/components-ai` — `terminal` and `ai` are layer-2 DAG siblings; the shared vocabulary (`SessionCapability`/`SessionWhatsNewItem`/`SessionQuickAction`) lives in `@elabs-ai/components-ui` instead.
- TerminalClearButton
- TerminalComposer — 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.
- 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).
- avoid: 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.
- avoid: 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.
- avoid: 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.
- avoid: 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.
- TerminalConsole — The console FRAME (ADR 0033): the one resting surface a coding-agent console draws — edge, radius, ground and elevation — so a transcript, banner, permission prompt, composer and status bar sit inside it as flush regions separated by a single seam instead of as separately framed cards.
- avoid: Stacking a second framed piece inside it — a region never draws its own radius, border, shadow or ground; ADR 0033's whole point is exactly one edge per console.
- avoid: Adding a `variant`, transcript state, expansion state or a clock to its context — it publishes one static boolean, forever, the same prohibition `TerminalSurface`'s `variant` context carries.
- avoid: Negating a region's frame with `rounded-none border-0 shadow-none` instead of omitting it — `TerminalSurface`'s frame-aware branch OMITS those classes; a caller who wants a framed region back adds them through `className`.
- avoid: Nesting a console inside another console — a split view of two sessions is two consoles side by side in a layout, never a frame inside a frame.
- TerminalContent
- TerminalCopyButton
- TerminalDiffHunk — An inline unified diff hunk in console dress: a header naming the file, then one row per line — a line-number column, a polarity marker, and the line text — with a collapsed run of context lines behind a real disclosure.
- avoid: Re-deriving the `"added: "`/`"removed: "` polarity words locally — they ride the promoted `diffLineAccessibleLabel()` shared with DiffView (`@elabs-ai/components-ai`); a second implementation drifts from it.
- avoid: Rendering the line number as a `ch`-unit padded string — it is a plain layout box in the content cell, never text-column arithmetic.
- avoid: Truncating a long line instead of wrapping it — `min-w-0` plus `whitespace-pre-wrap` on the text cell is what lets it wrap under the content column.
- avoid: Putting the collapsed-context-run's expansion in `TerminalSurface`'s context, or hand-rolling a one-way 'show more' button — it is a real, per-row Radix `Collapsible`, same as every other disclosure in this family.
- TerminalEventLine — The CLI dress of an agent lifecycle/hook event line: a fixed marker glyph, a label, and an optional phase/hook-count/duration, sharing AgentEvent's outcome and hook-count model so the two skins stay in sync.
- avoid: Importing AgentEvent or anything from `@elabs-ai/components-ai` — `terminal` and `ai` are layer-2 DAG siblings; the shared model lives in `@elabs-ai/components-ui` instead.
- avoid: Rendering the outcome word only when it is bad — `succeeded` gets the same glyph-plus-sr-only-word treatment as `blocked`/`failed`, never a silently unmarked default.
- avoid: Treating a `hooks` summary's `passed < ran` as merely informational — that mismatch is an independent failure signal (a distinct glyph plus an `sr-only` count), regardless of the row's own `outcome`.
- avoid: Renaming `hooks` to `checks` to match AgentEvent literally — the rendered vocabulary IS the literal upstream word "hooks", and this component's bracket is a terser summary, never AgentEvent's per-check breakdown list.
- TerminalFrameContext
- TerminalHeader
- TerminalOverlay — The console-dress modal frame: a real Radix Dialog painted on the terminal ground, with a title row, arbitrary caller content, and an optional key-hint legend along the bottom — a frame, not a catalogue of panels.
- avoid: Keeping DialogContent's baked-in close icon — it is styled with `text-muted-foreground`, a token calibrated for `--card`/`--background`, never the terminal ground (the same class of bug a sibling already had to repair on this ground); it is hidden and replaced with a close control styled from the terminal token group.
- avoid: Adding a `border` alongside the dialog's `shadow-ring-*` — an overlay floats; the elevation rule bans a border beside a ring shadow (the 'double edge').
- avoid: Rendering the footer's key glyphs as the only carrier of an action's meaning — each hint's action reaches assistive tech as real words; the glyph is decorative.
- avoid: Building the settings-modal catalogue itself here — this component is the ground, the dismissal and the legend only; the content is the caller's (a stated #117 non-goal).
- TerminalPermission — The per-call scoped approval prompt: title, command preview, question, then numbered options whose SCOPE (once/session/deny) is stated in real label text and chosen through a real Radix RadioGroup, never a hand-rolled focus walk.
- avoid: Importing ApprovalCardOptions/Confirmation or anything from `@elabs-ai/components-ai` — `terminal` and `ai` are layer-2 DAG siblings; the shared model (`ApprovalScope`/`ApprovalOption`/`APPROVAL_SCOPE_DESCRIPTION_KEYS`) lives in `@elabs-ai/components-ui` instead.
- avoid: Walking `parentElement.children[i]` for roving focus — a real Radix `RadioGroup` owns keyboard navigation, matching the family's fidelity axis.
- avoid: Naming a vendor product in the deny option's label — the third option declines and redirects the agent using the vendor-free `ApprovalScope` vocabulary, never the upstream product name (#117 acceptance criterion).
- avoid: Always rendering the reason field — it only reveals, through a real Radix `Collapsible`, once the deny-scoped option is the one selected.
- TerminalRow (variant=marker*|rail|boxed) — The two-column [gutter][content] grid primitive every console row is built from, with the gutter's meaning carried as words for assistive tech.
- avoid: Passing a meaningful glyph as `gutter` with no `gutterLabel` — the glyph is aria-hidden, so the row's meaning would exist only as decoration (WCAG 1.4.1).
- avoid: Aligning columns with `ch` units or padding — the gutter is a grid track, which is what makes a wrapped continuation line align for free.
- avoid: Dropping `min-w-0` from the content cell — without it, and the grid's `minmax(0,1fr)`, a long path pushes the row out of the surface instead of wrapping.
- avoid: Writing box-drawing characters as a border — a screen reader reads them aloud and they collapse at a font fallback; the `boxed` variant uses a real border.
- TerminalSlashMenu — 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.
- 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.
- avoid: 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.
- avoid: Calling `findTriggerQuery` with the default word boundary — a slash palette needs `{ boundary: "line-start" }`, or `cd /usr` would wrongly open the popup mid-command.
- avoid: 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.
- avoid: Querying the textarea with `querySelector` — `TerminalComposer`'s `textareaRef` prop exists precisely so a caret-tracking wrapper never needs to.
- avoid: 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`.
- TerminalStatus
- TerminalStatusBar — The ambient chrome row along the bottom of a console surface: branch and working directory on the left, connection/context/turn progress on the right, every fact independently optional.
- avoid: Reaching for `@elabs-ai/components-ai`'s `Context` component or its tokenlens math — `terminal` and `ai` are layer-2 DAG siblings; `context` takes two ALREADY-FORMATTED display strings the caller computed, never raw token counts to abbreviate.
- avoid: Relying on colour alone for `connections.disconnected` — it renders a distinct glyph (UnplugIcon, not PlugZapIcon) plus its own visible text label, recoverable in greyscale and by a screen reader.
- avoid: Adding a second `role="status"` on an inner segment — the container itself is already the one live region; a nested one duplicates announcements.
- avoid: Rendering the right-cluster divider as a literal `│` character — that is a box-drawing glyph as text; it is a plain `bg-terminal-border` rule instead.
- TerminalSurface — The console ground for the agent-session family: the terminal surface, the mono type role and the two-column gutter grid, established once and published to every row inside.
- avoid: Adding a second value to its context — it publishes `variant` and nothing else; transcript state, expansion state and a clock all belong to the caller or to the row (D5).
- avoid: Expecting it to scroll — it owns no scroll container on purpose, so a caller's virtualizer for a long transcript is never fought.
- avoid: Declaring `--terminal-gutter` in themes.css — it is a local layout custom property, not a theme token; it does not vary by theme.
- avoid: Reaching for a spinner while `loading` — the not-ready rung is layout-shaped skeleton rows rendered through the real TerminalRow, behind exactly one live region.
- TerminalTitle
- TerminalTodoList — A three-state checklist (done/active/pending) rendered as a real /, where each row's state is a glyph AND an announced word riding TerminalRow's gutterLabel, so it survives greyscale and reaches assistive tech.
- avoid: Minting a fourth `done|active|todo` union — it reuses `@elabs-ai/components-ui`'s `TimelineStatus` (`done|active|pending`) so the vocabulary can't drift from Timeline's.
- avoid: Rendering the state as a Lucide icon — the upstream grammar IS the literal ✔ / ◼ / ◻ characters; the fidelity axis reproduces that exactly.
- avoid: Hand-rolling a second `sr-only` span for the state word — it rides `TerminalRow`'s existing `gutterLabel`, which already suppresses the glyph correctly per variant.
- avoid: Reproducing upstream's `" ⎿ "` / four-space character padding — that is `ch`-unit alignment; `TerminalRow`'s gutter grid track does the same job without it.
- TerminalToolCall — A single tool invocation dressed as a CLI line: a status glyph (success/error/pending), the tool name plus its optional argument in parentheses, a result summary on a `⎿` row, and detail behind a real disclosure.
- avoid: Relying on the recoloured `⏺` alone to tell success from error — only `success` keeps that literal glyph; `error`/`pending` each get their own shape (`✗`/`○`) plus `gutterLabel`'s announced word, never colour alone.
- avoid: Setting `role="alert"` while `status="pending"` — a still-running call is not a settled failure (`.claude/rules/conventions.md`, Loading & streaming states); only `error` fires the alert.
- avoid: Reaching for the 7-state canonical `Status` or `TimelineStatus` — neither matches this grammar's exact `success`/`error`/`pending` vocabulary or its live-not-yet-settled meaning of `pending`.
- avoid: Reproducing upstream's inert `"(ctrl+o to expand)"` hint as decorative text — the expand affordance is a real, localized, focusable `CollapsibleTrigger`, so a keyboard user never needs to know a CLI chord.
- avoid: Putting expansion state in `TerminalSurface`'s context — it is per-row Radix `Collapsible` state, same as every other disclosure in this family.
- TerminalTranscriptRow (kind=user*|agent|output|error) — One line of an agent transcript on top of TerminalRow: a closed kind axis (user/agent/output/error) that carries who spoke and whether it failed as a glyph, a colour and an accessible label.
- avoid: Rendering `kind="error"` while a turn is still streaming — errors are for settled, terminal failures only (`.claude/rules/conventions.md`, Loading & streaming states).
- avoid: Relying on colour alone to distinguish `error` — the glyph and the `gutterLabel` word are the load-bearing channels; colour is redundant.
- avoid: Reaching for a vendor mode/effort union on `kind` — the axis is closed to `user`/`agent`/`output`/`error`, nothing else (#117 acceptance criterion).
- avoid: Formatting `exitCode` yourself into a string — pass the number and let the component render the affordance.
- TerminalWorking — The in-turn footer row pinned last in a transcript: a spinner/diamond glyph, a label, elapsed time, token spend and a stop control — the three facts and an exit the human needs while the agent runs.
- avoid: Running a setInterval to advance the spinner or tick elapsedMs — the spinner is a pure CSS keyframe and elapsedMs is a caller-supplied snapshot (D5); this component owns no timer.
- avoid: Announcing the ticking elapsed time or token count in the live region — exactly one role=status announces `label`; the stats are plain, non-live text or assistive tech is flooded on every re-render.
- avoid: Inventing a keyboard shortcut for the stop control — `stopShortcut` is caller-supplied (mirrors SessionHeader's/PermissionModeSelect's `keyHint`); the component never hardcodes a binding.
- avoid: Hand-rolling a second compact-number formatter for `tokens` — it renders through `useLocale().formatNumber`, not a bespoke compacter.
## Hooks
- useTerminalVariant