---
name: shell-chat
load-when: authoring a chat-shell LLM conversation surface
load-size: ~1.5k tokens
required-for: [shell-selection — chat path]
---

# chat-shell — the conversation surface

LLM chat chrome from `@adia-ai/web-modules`. Register: `import '@adia-ai/web-modules/chat'`. The **LLM client/proxy/security** lives in `llm-wiring` — this is the _surface_; that is the _wiring_. (Claims re-verified against the kit's live source 2026-08-26; re-verify on a MINOR cut.)

## Cluster roster

`<chat-shell>` (orchestrator) · `<chat-header>` (+ `<chat-status slot="status">`) · `<chat-thread>` (scrolling messages, reflects `[streaming]`) · `<chat-empty>` (empty-state slot) · `<chat-composer>` (input wrapper, disables while streaming) · `<chat-input-ui>` (**a web-components primitive, NOT in the chat barrel** — import it separately, see Gotchas) · `<chat-sidebar slot="sidebar">` (optional).

## Canonical skeleton

```html
<!-- model= omitted deliberately: @adia-ai/llm's DEFAULT_MODEL applies; current
     ids live in packages/llm/core/models.js — a pinned id here goes stale every
     model generation. Set model= only to override. -->
<chat-shell proxy-url="/api/chat">
  <chat-header><span slot="name">Assistant</span><chat-status slot="status"></chat-status></chat-header>
  <chat-thread>
    <chat-empty><empty-state-ui icon="chat-circle" heading="Hello!" description="Ask me anything."></empty-state-ui></chat-empty>
  </chat-thread>
  <chat-composer><chat-input-ui placeholder="Message…"></chat-input-ui></chat-composer>
</chat-shell>
```

## Props · events · methods

- **Props:** `model` · `provider` (anthropic|openai|gemini — chat-shell streams via `streamChat`, whose adapter registry THROWS on any other name; `google`/`stub` belong to the separate `llm-bridge.js` entry point, which chat-shell does not use) · `proxy-url` · `system` · `thinking` · reflects `[streaming]`.
- **Events:** `submit {text, model}` · `chunk {text, snapshot}` · `thinking {text}` · `done {text, usage, stopReason}` · `error {error}` · `abort` · `clear` · `message {id, role, content}`.
- **Methods:** `send(text, {model})` · `appendMessage({role, content})` · `appendChunk(text)` · `clear()` · `abort()` · `export()` / `import(data)`; accessors `conversation` / `messages`.

## Wiring to the LLM

Set `proxy-url` (or, dev-only, `apiKey`) and the shell **auto-sends on submit** via `streamChat` and renders the stream for you. Otherwise it just emits `submit` — you call your endpoint and drive the UI with `appendChunk`/`done`/`error`. **Security:** the production pattern is a same-origin smart proxy that holds the key server-side — never ship a provider key to the browser. Full client/proxy contract: `llm-wiring`.

## Gotchas

- Import the **chat barrel**; piecemeal imports leave children unregistered. But the barrel does NOT register `<chat-input-ui>` — it's a web-components primitive (`components/chat-thread/chat-input.js`), so the composer needs its own import alongside the barrel (real usage: `apps/genui/app/factory-chat/factory-chat.contents.js:12-13` imports both). Skipping it = an unregistered, 0px composer.
- `chat-input-ui` in turn internally renders `textarea-ui` + `select-ui` (also invisible in your authored HTML) — register those primitives too, or they stay undefined and collapse to 0px.
- Legacy shapes (`[data-chat-messages]`, `[data-chat-input]`, `[data-chat-empty]`, `[data-chat-name]`) were retired v0.4.0 — use the bespoke tags (`adia-lint` `LEGACY-SHELL`).
- SSR: register `<chat-shell>` client-side like any component; keep the key server-side.
- **Reasoning/trace panels must surface their own reliability** — a bare status label (`Domain: data`) reads identically at 3% and 95% confidence; a label that hides the data needed to judge it is pragmatically deceptive. Show the confidence with the claim.
- **A persistent `<drawer-ui data-mobile-nav-drawer>` is always in the DOM** (relocated mobile-nav mechanism, gh#1984/ADR-0090), not conditionally created — only its visibility responds to a container query at the shell's mobile-nav breakpoint. A selector assuming exactly one `drawer-ui`/`dialog` on the page (e.g. an E2E test) must scope past it: `drawer-ui:not([data-mobile-nav-drawer])`.

Real usage: `apps/genui/app/factory-chat/`.
