Chat Shell

chat-shell module-tier

Behavior-only chat orchestrator (LLM-streaming module). Author supplies the DOM structure via [data-chat-messages], [data-chat-input], [data-chat-empty], [data-chat-status] elements; chat-shell wires message streaming, markdown rendering, code-block upgrades, and an LLM integration path via proxy-url (or via external submit).

Basic shape (legacy)

Author provides the structural DOM; the shell binds the LLM streaming behavior and renders chunks into the messages container. This is the original raw-HTML authoring shape — still fully supported.

<chat-shell provider="anthropic" model="claude-sonnet-4-7" proxy-url="/api/llm"> <header> <span slot="heading">Chat</span> <span slot="description">claude-sonnet-4-7</span> </header> <div data-chat-messages></div> <div data-chat-empty> <p>Send a message to start.</p> </div> <div data-chat-input> <input-ui placeholder="Ask anything..." submit-on-enter></input-ui> </div> <div data-chat-status></div> </chat-shell>

Basic shape (bespoke — recommended)

The bespoke shape uses module-namespaced custom elements per ADR-0023. Each child owns its own behavior + state attributes; queryable from outside via :has(chat-thread[streaming]).

<chat-shell provider="anthropic" model="claude-sonnet-4-7" proxy-url="/api/llm"> <chat-header> <span slot="name">Claude</span> <chat-status slot="status">Connected</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…" submit-on-enter></chat-input-ui> </chat-composer> </chat-shell>

State as attribute

Every queryable state is reflected on the relevant child element. Style cross-cuts via :has():

/* Subtle indicator while LLM is streaming */ chat-shell:has(chat-thread[streaming]) chat-header { /* … */ } /* Disable composer style during streaming */ chat-shell:has(chat-composer[disabled]) { /* … */ } /* Empty-state visibility — driven automatically */ chat-thread:not([empty]) > chat-empty { display: none; }

JS reads the same attributes — shell.querySelector('chat-thread').streaming. The host (<chat-shell>) propagates [streaming] to the bespoke <chat-thread> child automatically when an LLM response is in flight.

Properties

PropTypeDefaultDescription
providerstringLLM provider name: anthropic | openai | google | stub.
modelstringModel identifier (e.g. claude-sonnet-4-7, gpt-4o, gemini-1.5-pro).
systemstringSystem prompt prepended to conversations.
proxy-urlstringAPI proxy endpoint for LLM calls; enables self-contained chat without external submit handler.
thinkingbooleanfalseEnable Anthropic extended-thinking mode.
streamingbooleanReflected — set while a response is streaming.

Events

EventDetail
submit{ text, model } — fired on user message submit before LLM call begins.
chunk{ text, role } — fired for each streaming chunk.
complete{ text } — fired when streaming finishes.
error{ error } — fired on LLM call failures.

Behavior wiring

AffordanceAuthor markupWhat the shell does
Message stream[data-chat-messages]Each LLM chunk appended; markdown rendered; code-ui upgraded for code blocks.
Input submit[data-chat-input] containing an <input-ui>Listens for input's submit event; calls LLM via proxy-url or fires submit for external handler.
Empty state[data-chat-empty]Hidden when messages are present; shown when empty.
Status indicator[data-chat-status]Updated with streaming/idle text.

Family pattern (forward-looking)

Per ADR-0023, future releases extend the bespoke-children family to the chat cluster. Planned children:

Today's data-* shape is fully supported and will continue to work alongside the bespoke vocabulary when it lands.