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

# `lr-activity-feed`

- **Import** `import '@aceshooting/lyra-ui/components/lr-activity-feed.js';` (stable tag alias; registers the tag)
- **Class** `LyraActivityFeed`, also available unregistered from `@aceshooting/lyra-ui/components/agent-tools/activity-feed/activity-feed.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** 17 parts, 8 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-activity-feed`

An append-only streaming log of granular agent actions ("Searching the web…", "Read the
application entry point"), collapsing to a localized "Completed N steps" summary once the run is over. Entries
never change state once added — a step whose status mutates in place belongs to `<lr-task-list>`
instead. Implements the shared follow (stick-to-bottom) contract. Focus is repaired rather than
silently dropped when the control holding it disappears: collapsing (`expanded` becoming `false`)
moves focus already inside the body to `[part="header"]` before the body is hidden, and removing
the specific `entries` row that held focus does the same once that render (and, while virtualized,
the internal `<lr-virtual-list>`'s own follow-up render) has settled. Focus that is elsewhere is
left alone — appending a live entry never steals focus from an unrelated, still-present control.
At/above `virtualizeAt`
entries, the body renders through an internal `<lr-virtual-list>` instead of a plain keyed list.

**Properties:** `entries: ActivityEntry[] = []` (attribute: false) — `ActivityEntry { id: string;
text: string; icon?: string; timestamp?: Date | string; variant?: LyraVariant; data?: unknown }`
(exported here). `icon` is a literal glyph hint (e.g. an emoji), the same convention
`lr-tool-call-chip.icon` uses; a small variant dot renders in its place when omitted. Empty/blank
ids and later duplicate ids are omitted before the summary, keyed render, or virtualization path is
chosen. `LyraVariant = 'neutral' | 'brand' | 'success'
| 'warning' | 'danger'` is the library-wide semantic vocabulary, so an entry is toned with the same
five values as every other `variant` in the library. An invalid `timestamp` string is treated as
unset. `data` is an opaque caller payload (e.g. the source record an entry summarizes) — never read
or rendered by this component. It is retained by reference, never deep-cloned, through the owned
`entries` snapshot, and handed back to `renderText` on every render, so a consumer needing richer
per-entry context does not have to re-derive it by re-scanning its own source array by id. `mode: 'live' | 'post-hoc' =
'live'` (reflected), `follow: boolean = true` (reflected), `expanded: boolean = false` (reflected),
`label?: string` — omission localizes `activityFeedLabel` (`'Activity'` in the built-in English
catalog), while any supplied string is a verbatim override, including `'Activity'` under a
non-English `.strings` catalog and `''`. A present host `aria-label` names the owned list in both
plain and virtualized rendering paths while `label` remains the visible header text. Host
`aria-labelledby` and `aria-describedby` likewise reach that semantic list in both rendering paths —
`showTimestamps: boolean = false` (attribute `show-timestamps`),
`formatTimestamp?: (date: Date) => string` (attribute: false), `renderText?: (entry: ActivityEntry)
=> TemplateResult` (attribute: false) — overrides the default plain-text `entry-text` rendering with
arbitrary rich content (e.g. rendered markdown, or markdown plus a trailing tool-call chip list),
identically whether or not the feed is currently virtualized; replaces the plain text **inside**
the persistent `[part="entry-text"]` wrapper rather than removing that part, and `virtualizeAt: number = 199` (attribute
`virtualize-at`). `compact: boolean = false` (reflected) — tighter header and entry-row padding for
dense transcript contexts, the same density-only convention `<lr-confirm-bar>`'s and
`<lr-thinking-panel>`'s own `compact` establish: the outer border and surface stay, so pair it
with `frame="plain"` to remove card chrome. Retune it through
`--lr-activity-feed-compact-header-padding`, `--lr-activity-feed-compact-header-gap`, and
`--lr-activity-feed-compact-entry-padding`. `frame: LyraFrame = 'card'` (reflected) — `'card' |
'plain'`, imported from the library's shared container-frame vocabulary and behaving exactly as
it does on `lr-confirm-bar`/`lr-thinking-panel`/`lr-agent-run`/etc.: `'plain'` removes the outer
border, background, and corner radius so a feed nested inside existing message chrome doesn't
double it. The header/body divider and entry-row padding are unaffected by `frame` — only the
outer card goes.

**Events:** `lr-toggle` (`detail: { expanded }`, the header was activated) and
`lr-follow-change` (`detail: { following }`, `follow` released or re-engaged).

**CSS parts:** `base`, `header` (a `<button>`), `status-dot` (pulses while `mode="live"`), `label`,
`summary`, `toggle`, `body` (the scrollable region, or the internal virtual-list), `entry` (carries
`data-variant`), `entry-icon`, `variant-dot` (the dot rendered inside `entry-icon` when the entry
sets no literal `icon`), `variant-dot-neutral`/`variant-dot-brand`/`variant-dot-success`/
`variant-dot-warning`/`variant-dot-danger` (each also carries `variant-dot`), `entry-text`, and
`entry-timestamp` (only while `showTimestamps` and a valid `timestamp` is set). Every entry-level
part is reachable in both rendering paths, virtualized or not.

**Themeable custom properties:** `--lr-activity-feed-max-height` (default `16rem`) — cap on how
tall the expanded body grows before it scrolls internally; and
`--lr-activity-feed-live-status-color` (default `var(--lr-color-brand)`) — background color of
`status-dot` while `mode="live"`, independently retunable without changing other brand surfaces.
The `compact` density is retunable through three properties: `--lr-activity-feed-compact-header-padding`
(default `var(--lr-space-2xs) var(--lr-space-s)`) and `--lr-activity-feed-compact-header-gap`
(default `var(--lr-space-2xs)`) both scoped to `[part="header"]` while `compact`, and
`--lr-activity-feed-compact-entry-padding` (default `var(--lr-space-2xs) var(--lr-space-s)`)
scoped to `[part="entry"]` while `compact`. All three are inline `var()` fallbacks at their point
of use, so any can be set on the element or on an ancestor, same as `lr-confirm-bar`'s and
`lr-thinking-panel`'s own compact tokens. The card chrome itself is retunable the same way:
`--lr-activity-feed-background` (default `var(--lr-color-surface)`) fills `[part="base"]`,
`--lr-activity-feed-border-color` (default `var(--lr-color-border)`) colors both its border and the
header/body divider that `frame="plain"` keeps, and `--lr-activity-feed-radius` (default
`var(--lr-radius)`) sets its corner radius — so retuning a nested feed no longer needs a
`::part(base)` override. `frame="plain"` still removes the border, radius and fill outright; the
hooks tune the card presentation rather than reinstating chrome you asked to drop.

**Known gotchas:**

- The variant dot's color is selected by its _part name_, not by `[data-variant]`: `::part()` cannot
  be followed by an attribute selector, so
  `lr-activity-feed::part(variant-dot)[data-variant='success']` never matches. Target
  `lr-activity-feed::part(variant-dot-success)` instead. `data-variant` remains on both the entry
  and the dot for DOM queries.
- `compact`/`frame` render byte-identically to the pre-existing default when unset — neither
  property changes anything about the plain-card presentation.
- Focus repair after an `entries` change is asynchronous while virtualized (it waits on the
  internal `<lr-virtual-list>`'s own follow-up render before deciding whether the previously
  focused row actually disappeared), but synchronous when collapsing (`expanded` going `false`)
  — the header renders regardless of `expanded`, so there's nothing to wait for there.
