<!-- GENERATED by scripts/build-llms.mjs from llms/agent-tools.md — do not edit this file. -->

# `lr-terminal`

- **Import** `import '@aceshooting/lyra-ui/components/lr-terminal.js';` (stable tag alias; registers the tag)
- **Class** `LyraTerminal`, also available unregistered from `@aceshooting/lyra-ui/components/agent-tools/terminal/terminal.class.js`
- **Family** `components/agent-tools/` — see `llms/index.md` for its siblings
- **Status** `stable` since `4.0.0` — see the maturity and deprecation policy in `llms/shared.md`
- **Release history** [CHANGELOG.md](../../CHANGELOG.md); family-wide breaking-change summaries: [llms-full.txt](../../llms-full.txt)
- **Deprecations** none
- **Optional peers** none
- **Themeable via** 16 parts, 18 custom properties — see this component's own `@csspart`/`@cssprop` list below
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-terminal`

A read-only ANSI console for streamed agent/tool output. Not a PTY: no stdin/keystroke handling, no
cursor-addressed full-screen apps. An ANSI sequence split across chunks retains at most 4,096
characters; an overlong unterminated CSI/OSC sequence is dropped and the next write resumes from a
clean parser boundary.

A search with no matches clears earlier rendered match markers. Removing the `content` attribute
clears output and preserves the normal `null` property readback.

**Properties:** `content: string = ''` — initial/replaceable buffer content, parsed for ANSI/SGR
codes. `replace(content: string): void` synchronously replaces the parsed buffer and reactive
`content` source, preserving commit order with same-turn `write()`/`clear()` calls.
`maxScrollback: number = 5000` (attribute `max-scrollback`), `follow: boolean = true`
(reflected) — stick-to-bottom, `wrap: boolean = true` (reflected), `copyable: boolean = true`
(reflected) and `downloadable: boolean = false` (reflected) toggle the toolbar buttons, `filename:
string = 'terminal.log'`, `announceOutput: boolean = false` (attribute `announce-output`),
`accessibleLabel: string = ''` (attribute `aria-label`), `highlights: readonly LyraHighlight[] = []` (attribute:
false), and `activeHighlightId: string | null = null` (attribute: false). Empty/blank highlight ids
and later duplicates are omitted before painting, focus ownership, active lookup, and activation
events. A non-empty host `aria-label` is forwarded to the nested `role="log"`; an absent or explicit
empty value uses the localized terminal-purpose fallback, so the actionable log remains named.
`compact: boolean = false` (reflected) — tightens `[part="toolbar"]`'s padding and gap and each
rendered line's inline padding for a terminal embedded in an already-padded transcript row, the same
convention `<lr-task-list>` and `<lr-thinking-panel>` use; purely a density knob, the card border and
background stay. `frame: LyraFrame = 'card'` (reflected) — container treatment in the library-wide
`frame` vocabulary (`'card' | 'plain'`); `'plain'` removes `[part="base"]`'s border, corner radius,
and raised surface so a terminal nested inside a container that already draws a border (an agent-run
panel, a message bubble) doesn't double it, while keeping the toolbar/log divider and whichever
regular or compact padding applies. `anchorKinds:
LyraAnchor['kind'][] = ['line-range']` is readonly — a scrollback buffer addresses positions by line number, so `line-range` is the
only kind `scrollToAnchor()` resolves; `page`/`text-quote`/`region` belong to the paginated document
viewers, not here. `<lr-terminal>` is not registered in the document-renderer registry, so this field
is a plain readonly property rather than the `DocumentAnchorTarget` mixin's `override readonly` one.

**Methods:** `write(text)` appends ANSI-parsed text to the buffer, subject to the bounded partial
sequence behavior above. `clear()` empties the buffer.
`scrollToBottom()` and `scrollToAnchor(anchor): Promise<boolean>` control scroll position.
`search(query): Promise<number>` (resolves the match count after the resulting render),
`searchNext()`, `searchPrevious()`, and `clearSearch()` drive in-buffer text search — matching is
line-granular (a match identifies a whole line, not a character range) and capped, so `matchCount`
stops climbing on a pathologically repetitive buffer. `getPlainText()` returns the SGR-stripped
plain text of the whole buffer.

**Events:** `lr-copy` (`detail: { ok: true, text }`, emitted only after a successful clipboard write),
`lr-error` (no detail) and `lr-copy-error` (`detail: { ok: false, text, reason, error }`) on clipboard failure,
`lr-download` (`detail: { filename }`, cancelable — by
default the component creates a plain-text `Blob`/object URL and activates a synthetic
`<a download>`; `preventDefault()` suppresses that built-in download so the host can substitute
server-side or other handling),
`lr-follow-change` (`detail: { following }`), `lr-search-change` (`detail: { query, matchCount,
matchCountExact, activeIndex }`; `matchCountExact` is `false` once a search hits the 10,000-match
ceiling, marking `matchCount` as a lower bound rather than an exact total),
`lr-highlight-activate` (`detail: { highlightId }`), and `lr-text-select` (`detail: {
text, anchor, rects }`).

**CSS parts:** `base`, `toolbar` (only rendered when copy/download are enabled), `copy-button`,
`download-button`, `viewport` (the `role="log"` scrollable region), `line` (one rendered line; carries
`data-line-number`/`data-match`/`data-highlight-tone`, and is forwarded via `exportparts` so
`lr-terminal::part(line)` reaches the rendered lines from a consumer stylesheet despite them living
in the internal `<lr-virtual-list>`'s shadow root), `jump-to-latest` (shown while `follow` is
disengaged and new output has arrived), `line-interactive`, `line-highlight-accent`,
`line-highlight-success`, `line-highlight-warning`, `line-highlight-danger`, `line-highlight-neutral`,
`line-match`, `line-active-match`, and `announcer` (the visually-hidden, `aria-hidden` mirror
of the text last announced while `announce-output` is set).

`[part="announcer"]` is a styling and inspection surface only — it carries **no** live-region role
of its own. The announcement itself goes to the library's shared **light-DOM** polite region,
appended to the consumer's `<body>` and marked `data-lr-live-region="polite"`, because a live
region inside a shadow root is not reliably announced (JAWS with Firefox ignores one outright).
Inspect the shared region, not `::part(announcer)`, when reading an announcement; the part remains
the right hook for styling and for reading back what the terminal last announced.

**Themeable custom properties:** `--lr-terminal-height` (default `var(--lr-size-20rem)`) — the
viewport's block size; not declared on `:host`, so it is inherited from the host or any ancestor.
`--lr-terminal-surface-color` (default `var(--lr-color-surface-raised)`) controls the card-frame
background and the fallback foreground for inverse ANSI segments without an explicit background;
`frame="plain"` remains transparent. `--lr-terminal-border-color` (default
`var(--lr-color-border)`) and `--lr-terminal-radius` (default `var(--lr-radius)`) complete that card
chrome — the border-color hook also colors the toolbar/log divider `frame="plain"` keeps.
`--lr-terminal-surface-color` keeps its established name; nothing was renamed.
`--lr-terminal-toolbar-button-hover-bg` (default
`var(--lr-color-brand-quiet)`) and `--lr-terminal-toolbar-button-active-bg` (default
`color-mix(in oklab, var(--lr-terminal-toolbar-button-hover-bg, var(--lr-color-brand-quiet)),
var(--lr-color-mix-partner) var(--lr-color-mix-active))`) control the copy and download buttons.
`--lr-terminal-line-hover-bg` (default `var(--lr-color-brand-quiet)`) and
`--lr-terminal-line-active-bg` (default `color-mix(in oklab, var(--lr-terminal-line-hover-bg,
var(--lr-color-brand-quiet)), var(--lr-color-mix-partner) var(--lr-color-mix-active))`) control
ordinary rendered lines. `--lr-terminal-highlight-accent-bg` (default `var(--lr-color-brand-quiet)`),
`--lr-terminal-highlight-success-bg` (default `var(--lr-color-success-quiet)`),
`--lr-terminal-highlight-warning-bg` (default `var(--lr-color-warning-quiet)`),
`--lr-terminal-highlight-danger-bg` (default `var(--lr-color-danger-quiet)`), and
`--lr-terminal-highlight-neutral-bg` (default `var(--lr-color-surface)`) — the background of a
`highlights[]` entry of the matching `tone`. `--lr-terminal-compact-toolbar-padding` (default
`var(--lr-space-2xs) var(--lr-space-xs)`) and `--lr-terminal-compact-toolbar-gap` (default
`var(--lr-space-2xs)`) retune `[part="toolbar"]`'s padding and button gap while `compact`, and
`--lr-terminal-compact-line-padding-inline` (default `var(--lr-space-xs)`) retunes each rendered
line's inline padding while `compact` — all three sit behind inline `var()` fallbacks, so a
transcript can retune every nested terminal at once without restating the rules. Each highlight
background is decoupled from the identical shared token it
falls back to (e.g. `accent`'s `--lr-color-brand-quiet` is also the copy/download-button hover tint)
so retinting one tone doesn't repaint the other surface reading that token, and from any
`::part('line')` stylesheet override — the background is applied inline, so a stylesheet rule can't
beat it without `!important`.

**The ANSI/SGR palette is two token sets, not one.** SGR gives the sixteen colour names two
different jobs, and each job is themed separately:

- `--lr-terminal-color-<name>` — **foregrounds**, i.e. `CSI 30`–`37` and `CSI 90`–`97`, drawn _on_
  the terminal panel.
- `--lr-terminal-bg-<name>` — **backgrounds**, i.e. `CSI 40`–`47` and `CSI 100`–`107`, drawn _under_
  the panel's text.

`<name>` is `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white` and their
`bright-` counterparts — 32 tokens in all, each with its own `--lr-theme-terminal-color-*` /
`--lr-theme-terminal-bg-*` retheme hook, and each with a separate light- and dark-mode value.

The two sets exist because each is solved against a different reference, which is what makes the two
cases a program cannot avoid legible:

1. every `--lr-terminal-color-*` clears 4.5:1 against `--lr-color-surface-raised` — the panel
   `<lr-terminal>` paints for itself — so **any foreground is legible on the panel**;
2. every `--lr-terminal-bg-*` clears 4.5:1 against the panel's default text colour, which is the
   foreground actually in effect whenever a program sets a background and no explicit colour, so
   **the default foreground is legible on any background**.

A single shared set could not do both: foregrounds solved against a light panel are all dark, so
`ESC[41m` would paint a near-black red behind near-black text. An _explicit_ foreground+background
pair (`ESC[30;47m`) is the emitting program's choice and is not guaranteed here, exactly as in a
native terminal — sixteen against sixteen is 256 combinations, several degenerate by construction.

Each colour keeps its canonical ANSI hue (a terminal's red has to look like red, or escape sequences
stop meaning what every other terminal makes them mean); only lightness is solved for. The
consequence worth stating: on a light panel every background is a light tint, so `ESC[40m` ("black
background") renders as the darkest tint that still leaves the default text readable rather than as
literal black. Extended-colour sequences follow the same split: 256-colour indices 0–15 resolve to
the role-matching named token (so `ESC[48;5;1m` gets the background red, not the foreground one),
while indices 16–255 and truecolor become literal `rgb()` values — those are content-supplied rather
than token-driven, and carry no contrast guarantee.

**Additional API surface:**

- `--lr-terminal-search-outline-color` — Outline color for a line containing a non-active search match. Default: `var(--lr-color-warning)`.
- `--lr-terminal-search-active-outline-color` — Outline color for the active search match's line. Default: `var(--lr-color-brand)`.

While a search query is active, writes, scrollback trimming, `content` replacement, and `clear()`
recompute the exact match count and emit `lr-search-change` only when that public search snapshot
actually changes. Pending output announcements are canceled by clear/replacement, disabling
`announceOutput`, or disconnect, so stale text is never announced after it has been removed.
A multi-line highlight paints every retained covered line but exposes exactly one keyboard/click
owner at the anchor's start, or at the first surviving covered line after scrollback trims that
start. Its accessible name combines the caller label with visible line text (or a localized line
number for an empty line), avoiding duplicate tab stops for one logical highlight.
