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

# `lr-streaming-text`

- **Import** `import '@aceshooting/lyra-ui/components/lr-streaming-text.js';` (stable tag alias; registers the tag)
- **Class** `LyraStreamingText`, also available unregistered from `@aceshooting/lyra-ui/components/conversation/streaming-text/streaming-text.class.js`
- **Family** `components/conversation/` — 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** `dompurify`, `katex`, `marked`, `shiki` — see `llms/peers.md`
- **Themeable via** 13 parts, 2 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-streaming-text`

A token-coalescing incremental text renderer for streaming assistant output, with an optional
blinking cursor and auto-detected Markdown rendering. First-party invention (no Web Awesome
equivalent). The host is expected to assign the _entire_ current text on every update to `content`,
not a delta — this component does no accumulation or ordering of its own.

**Properties:**

- `content: string = ''` — the full current text so far.
- `streaming: boolean = false` (reflected) — shows the blinking cursor after the rendered text;
  reflects so a host can also target `lr-streaming-text[streaming]` in CSS.
- `coalesceMs: number = 50` (attribute `coalesce-ms`) — trailing-edge coalesce window, in ms, for
  `content` updates (see prose below).
- `contentMode: StreamingTextContentMode = 'auto'` (attribute `content-mode`, reflected) — `auto`
  uses `looksLikeMarkdown`; `plain` and `markdown` force their named paths. Invalid values render as
  `auto` without installing a stale memoized decision.
- `languages?: Readonly<Record<string, ShikiLanguageInput>>` (property only) — forwarded verbatim to
  the composed `<lr-markdown>`'s own `languages`, the same fine-grained language-grammar scoping
  `<lr-code-block>`/`<lr-markdown>` support. Unset leaves the composed element's own default
  untouched.

The rest of `<lr-markdown>`'s configuration surface forwards verbatim too, each defaulting to
exactly `<lr-markdown>`'s own default so leaving it unset renders identically to before these
properties existed:

- `tabSize: number = 4` (attribute `tab-size`) — forwarded to the composed `<lr-markdown>`'s own
  `tabSize`.
- `htmlMode: 'sanitize' | 'escape' | 'trusted' = 'sanitize'` (attribute `html-mode`) — forwarded to
  the composed `<lr-markdown>`'s own `htmlMode`.
- `gfm: boolean = true` — forwarded to the composed `<lr-markdown>`'s own `gfm`.
- `linkTarget: string | null = '_blank'` (attribute `link-target`) — forwarded to the composed
  `<lr-markdown>`'s own `linkTarget`; the composed element always applies its own
  `rel="noopener noreferrer"` guard whenever a `target` is emitted, including a forwarded
  non-default value, and never a bare `opener`.
- `internalLinkPrefix: string = ''` (attribute `internal-link-prefix`) — forwarded to the composed
  `<lr-markdown>`'s own `internalLinkPrefix`.
- `headingOffset: number = 0` (attribute `heading-offset`) — forwarded to the composed
  `<lr-markdown>`'s own `headingOffset`.
- `highlightCode: boolean = true` (attribute `highlight-code`) — forwarded to the composed
  `<lr-markdown>`'s own `highlightCode`.
- `headingAnchors: boolean = false` (attribute `heading-anchors`) — forwarded to the composed
  `<lr-markdown>`'s own `headingAnchors`.
- `math: boolean = false` — forwarded to the composed `<lr-markdown>`'s own `math`; the transitive
  `katex` peer is requested only once this is set.
- `maxHeight: string = ''` (attribute `max-height`) — forwarded to the composed `<lr-markdown>`'s
  own `maxHeight`.

**Exported helper:** `looksLikeMarkdown(text: string): boolean` — runs a fixed, ordered list of
lightweight regexes (ATX heading, fenced code block, `**bold**`, `_italic_`, inline code, bullet
list item, numbered list item, `[text](url)` link, blockquote) against the whole string and returns
`true` on the first match. Used internally in `contentMode="auto"`; exported standalone
so the heuristic is directly testable without going through the component's render cycle. None of
the patterns need to be airtight — a false positive just routes ordinary prose harmlessly through
`<lr-markdown>`; a false negative just shows literal `**`/backticks/etc. as plain text until more
of the stream arrives.

**Events:** `lr-content-settled` (`detail: null`, composed, bubbling) — fired once newly-coalesced
`content` actually reaches the rendered DOM. In `markdown` mode (forced or auto-detected) this
element does not emit the event itself; the composed `<lr-markdown>` it delegates rendering to
already emits its own `lr-content-settled` at its own settle point, and — being composed — that
event bubbles out through this element unmodified, so exactly one event per settle reaches a
listener either way. A consumer composing this element inside a free-form container (e.g.
`<lr-thinking-panel>`'s default slot) can listen for it to drive auto-scroll, since this component
renders into its own shadow root and a plain light-DOM `MutationObserver` on the container can
never see that update happen. See `<lr-thinking-panel>`'s own reference at `llms/components/lr-thinking-panel.md`.

**Slots:** none — content renders from `content`, not a slot.

**CSS parts:** `base`, `cursor` (only rendered while `streaming` is `true`), plus every part
`<lr-markdown>` documents forwarded verbatim (no aliasing — none collides with `base`/`cursor`)
from the composed `<lr-markdown>` in Markdown mode via `exportparts`: `content`, `heading`,
`paragraph`, `list`, `code-block`, `inline-code`, `link`, `table`, `blockquote`, `img`, `math`. A
host-level `lr-streaming-text::part(link)`/`::part(img)` rule reaches the rendered `<a>`/`<img>`
exactly as the same rule does applied directly to `<lr-markdown>`.

**Themeable custom properties:** `--lr-inline-cursor-width` (default
`var(--lr-size-0-125rem)`, the shared inline cursor width), `--lr-inline-cursor-height` (default
`var(--lr-size-1em)`, so the bar tracks surrounding text). These are shared with
`<lr-typing-indicator>`, inherit from ancestors, and use local fallbacks only at the point of use; plus shared
`--lr-space-xs` (cursor's `margin-inline-start`) and `--lr-transition-ambient` (blink animation
cycle length).

**Optional peer deps:** the registration entry imports and auto-registers `<lr-markdown>` (the host
does not register it separately), so its optional-peer module graph includes `marked`, `dompurify`,
`shiki`, and `katex`. The runtime matrix is narrower: `content-mode="plain"` and auto-detected plain text
stay on the peer-free plain-text path; Markdown rendering lazy-loads `marked` plus the default
`dompurify` sanitizer and falls back to readable plain text if either is unavailable. Fenced code
can additionally use `shiki`, whose absence only leaves code unhighlighted. The composed Markdown
implementation contains the opt-in `katex` loader; this wrapper forwards its own `math` property
(default `false`) to the composed element verbatim, so `katex` is requested only once a consumer
sets `math` here.

```html
<lr-streaming-text id="out" coalesce-ms="80" streaming></lr-streaming-text>
<script type="module">
  const out = document.getElementById("out");
  let text = "";
  for await (const token of tokenStream) {
    text += token;
    out.content = text; // always the full string so far, never a delta
  }
  out.streaming = false; // forces the final chunk through immediately; cursor disappears
</script>
```

Token-by-token streaming can update `content` far faster than a human can usefully perceive a
re-render, so updates use a shared coalescing timer. Within any `coalesce-ms` window, only the
_last_ `content` value
assigned actually reaches the rendered DOM. Two cases always bypass the throttle and flush
immediately: the very first `content` assignment after mount, and any transition of `streaming`
between `true` and `false` in _either_ direction — so the final chunk of a finished stream can
never be left stranded mid-window, and a stream restarting on a reused element can never keep
showing the previous stream's stale final content for the length of the window.

Rendering itself is never reimplemented here: Markdown mode composes `<lr-markdown>` directly,
forwarding this component's own `streaming` through as that component's `streaming` hint prop,
`languages` verbatim, and the rest of `<lr-markdown>`'s configuration surface verbatim too
(`tabSize`, `htmlMode`, `gfm`, `linkTarget`, `internalLinkPrefix`, `headingOffset`,
`highlightCode`, `headingAnchors`, `math`, `maxHeight` — see **Properties** above); plain-text mode
renders into a `white-space: pre-wrap` span instead. The
blinking cursor degrades
to a static, always-visible bar under `prefers-reduced-motion: reduce`. In plain-text mode it sits
inline at the tail of the final character; in Markdown mode it renders as its own trailing block
below the rendered content instead of attempting to splice into whatever nested block Markdown
happens to end with.

**Known gotchas:**

- `content` must always be the complete string so far, never a delta — this component does no
  accumulation of its own.
- Only the very _first_ `content` assignment after mount bypasses `coalesceMs` unconditionally —
  every later assignment is throttled normally except when it lands in the same update as a
  `streaming` transition (either `true → false` or `false → true`), which also forces an immediate
  flush.
- `content-mode="plain"` forces plain text even if the text obviously contains Markdown syntax;
  `content-mode="markdown"` forces the Markdown path.
- Purely presentational: no events, and it does not announce anything to assistive tech itself — a
  host that needs streamed text announced needs `<lr-live-region>` for that (e.g. composed inside
  `<lr-chat-message>`).

---
