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

# `lr-agent-workspace`

- **Import** `import '@aceshooting/lyra-ui/components/lr-agent-workspace.js';` (stable tag alias; registers the tag)
- **Class** `LyraAgentWorkspace`, also available unregistered from `@aceshooting/lyra-ui/components/conversation/agent-workspace/agent-workspace.class.js`
- **Family** `components/conversation/` — see `llms/index.md` for its siblings
- **Status** `stable` since `4.2.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** 16 parts, 0 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-agent-workspace`

Responsive, fully controlled shell for an AI conversation and its supporting run, tool, retrieval,
grounding, and context state: transcript + composer in the main pane, and a details pane composing
`lr-agent-run`, `lr-tool-timeline`, `lr-retrieval-results`, `lr-grounding-summary`, and
`lr-context-inspector`. Performs no network requests, model calls, retrieval, or persistence —
assign new data to the properties as the host receives updates.

This is the single component that binds the most of the provider-neutral vocabulary exported from
`@aceshooting/lyra-ui/ai` at once; a host that already holds `ChatMessage[]`,
`AgentRun`, `RetrievalChunk[]`, `Citation[]`, and `GroundingAssessment` can wire this up with no
adapters.

**Properties (transcript):**

- `messages: ChatMessage[] = []` (attribute: false) — **`ChatMessage` from
  `@aceshooting/lyra-ui/ai`**: `{ id: string; role: ChatMessageRole; status?: ChatMessageStatus;
timestamp?: Date | string; text?: string; attachments?: DocumentRef[]; parts?: MessagePart[];
metadata?: Record<string, unknown> }`. Each entry renders as an `lr-chat-message` whose
  `role`/`status`/`timestamp` come straight across. A nonempty `parts` array renders in order through
  `lr-message-parts` and takes precedence over the legacy `text` shortcut; otherwise `text` renders
  as sanitized Markdown through `lr-markdown`. Replace the whole region with the `messages` slot for
  richer bodies. Empty ids and later duplicates are omitted first-wins before the latest 500 valid
  identities are chosen, so malformed tail rows cannot evict earlier valid messages. Host owns
  ordering, updates, and persistence
- `follow: boolean = true` (reflected) — forwarded to the internal `lr-chat-viewport`
- `unreadStartIndex: number | null = null` (attribute `unread-start-index`) — forwarded to the viewport

**Properties (details pane):**

- `run: AgentRun | null = null` (attribute: false) — **`AgentRun` from `@aceshooting/lyra-ui/ai`**:
  `{ id: string; status: AgentStatus; startedAt?: number; endedAt?: number; model?: string;
costEstimate?: number; steps: AgentStep[] }` (epoch-ms timestamps). `null` omits the run section
- `metrics: AgentRunMetric[] = []` (attribute: false) — `lr-agent-run`'s own
  `AgentRunMetric { id: string; label: string; value: string | number; variant?: BadgeVariant }`,
  e.g. token counts or latency
- `tools: ToolTimelineEntry[] = []` (attribute: false) — `lr-tool-timeline`'s
  `ToolTimelineEntry extends ToolInvocation` (i.e. `{ id, name, args, status, result?, error? }` from
  `@aceshooting/lyra-ui/ai`) plus `{ startedAt?: number; endedAt?: number; retryCount?: number;
redactedFields?: string[]; needsApproval?: boolean; approved?: boolean }`
- `retrievalChunks: RetrievalChunk[] = []` (attribute: false) — **`RetrievalChunk` from
  `@aceshooting/lyra-ui/ai`**: `{ id, text, score, source: DocumentRef, metadata? }`, forwarded to
  `lr-retrieval-results`
- `selectedRetrievalChunkIds: string[] = []` (attribute: false) — controlled selection forwarded to
  `lr-retrieval-results.selectedChunkIds`
- `retrievalLoading: boolean = false` (attribute `retrieval-loading`), `retrievalHasMore: boolean =
false` (attribute `retrieval-has-more`), `retrievalErrorText: string = ''` (attribute
  `retrieval-error-text`, caller-supplied text) — all forwarded to `lr-retrieval-results`
- `groundingAssessment: GroundingAssessment | null = null` (attribute: false) — **`GroundingAssessment`
  from `@aceshooting/lyra-ui/ai`**: `{ supportedClaims, unsupportedClaims, coverage, confidence?,
warnings? }`
- `citations: Citation[] = []` (attribute: false) — **`Citation` from `@aceshooting/lyra-ui/ai`**,
  shown alongside the grounding summary
- `contextSegments: ContextInspectorSegment[] = []` (attribute: false) — `lr-context-inspector`'s
  `{ id: string; label: string; text: string; tokens: number; tone?: ContextMeterTone; citation?:
Citation; truncated?: boolean; omittedTokens?: number; redactions?: ContextInspectorRedaction[] }`
- `contextTotal: number = 0` (attribute `context-total`) — the overall context-window token budget
- `showDetails: boolean = true` (attribute `show-details`, reflected) — whether the details pane is
  available at all when data is present

**Properties (composer / chrome):**

- `showComposer: boolean = true` (attribute `show-composer`, reflected) — whether the built-in
  plain-frame composer renders when no `composer` slot is supplied. Its workspace-owned dock supplies
  the border and padding; a supplied `composer` slot keeps its own frame.
- `composerValue: string = ''` (attribute `composer-value`) — controlled composer value
- `composerStatus: ChatComposerStatus = 'idle'` (attribute `composer-status`) — `'idle' | 'sending' |
'streaming'`, `lr-chat-composer`'s own union; invalid values read as `idle` without rewriting the
  host attribute
- `composerPlaceholder: string = ''` (attribute `composer-placeholder`)
- `composerMinRows: number = 1` (attribute `composer-min-rows`), `composerMaxRows: number = 8`
  (attribute `composer-max-rows`)
- `label: string = ''` — accessible name and visible heading
- `accessibleLabel: string | null = null` (attribute `aria-label`) — host-level accessible-name
  override for the internal `role="region"` root

**Events:**

- `lr-input` (`detail: { value: string }`) / `lr-submit` (`detail: { value: string }`) / `lr-stop`
  (`detail: null`) — forwarded from the built-in composer.
- `lr-message-retry` (`detail: { messageId: string }`) — a data-driven message's retry action.
- `lr-follow-change` (`detail: { following: boolean }`) — forwarded from the transcript viewport.
- `lr-retrieval-select` (`detail: RetrievalResultsSelectDetail` = `{ chunkIds: string[]; chunks:
RetrievalChunk[] }`) — forwarded from the built-in retrieval results.
- `lr-citation-select` (`detail: CitationSelectEventDetail` = `{ citation: Citation }`, from
  `@aceshooting/lyra-ui/ai`) — forwarded from the built-in grounding summary.
- `lr-tool-approval-decide` (`detail: ToolTimelineApprovalDetail` = `ToolApprovalEventDetail &
{ args?: unknown }` = `{ invocationId: string; approved: boolean; args?: unknown }`) — forwarded
  from the built-in tool timeline; `args` is present only on approval and may differ from what the
  entry originally proposed (the dialog's inline edit step).
- `lr-cancel` (`detail: CancelEventDetail = { reason?: string }`) / `lr-run-retry` (`detail: RetryEventDetail` =
  `{ attempt: number; messageId?: string }`, from `@aceshooting/lyra-ui/ai`) — forwarded from the
  built-in agent run. The distinct retry name prevents a rendered message or attachment retry from
  being mistaken for a whole-run retry.

**Slots:** `messages` (replaces the data-driven transcript message list; assign ordinary messages
directly, or exactly one `lr-virtual-list` when the slot itself owns virtualization), `details` (replaces the
built-in run/tool/retrieval/grounding/context details pane while keeping the responsive shell),
`composer` (replaces the built-in plain-frame `lr-chat-composer`; supplied content keeps its own
frame), `header-actions` (model selection, settings, export controls).

**CSS parts:** `base`, `header`, `heading`, `header-actions`, `body`, `conversation`, `viewport` (the
composed `lr-chat-viewport`), `messages`, `messages-empty`, `details`, `details-content`, `section`
(one run/tools/retrieval/grounding/context section), `section-heading`, `composer`, `composer-input`,
`message`
(the composed `lr-chat-composer`).

**Themeable custom properties:** shared tokens only.

**Optional peer deps:** none of its own; the composed `lr-markdown` keeps its `marked`/`dompurify`
optional-peer fallback.

Every public data/value property is controlled: forwarded child intents bubble without mutating
`messages`, `composerValue`, selections, run state, or persistence-owned data inside the shell.

**Only the transcript scrolls.** `[part='base']` is a three-row grid — header, conversation,
composer — and only the middle row can shrink, so the composed `lr-chat-viewport` owns the
scrolling and the chrome rows stay put at their content size. The trade: give the workspace less
block-size than the chrome needs and the conversation row collapses to zero first, after which the
header or composer is clipped with no scrollbar. That takes unusually large slotted chrome — a very
tall `header-actions` toolbar, or a `composer` replacement much taller than the built-in one — and
the region that supplied it caps and scrolls itself through the public parts:

```css
lr-agent-workspace::part(header) {
  max-block-size: 4rem;
  overflow: auto;
}
```

The same applies to `::part(composer)`. Both declarations are needed: the cap keeps the grid track
inside the workspace, and `overflow` makes the capped region scrollable instead of spilling. No
component-owned custom property duplicates this, because a `::part()` rule from your tree already
wins over the shadow stylesheet regardless of specificity.
