AI & chat
Chat
Conversation history, composer and message states
detent-chat
Port status
- Catalog status
- implemented
- Origin
- upstream
- Phase
- 3
- Wave
- phase-3-data
- Group
- AI & chat
9.7 KB gzip for the class module
Variants
No messages yet — ask something to start the thread.
A full conversation (thread sidebar, grouped history, attachments, delivery states and retry) plus the empty state
using the empty-placeholder slot from light DOM. Below 768px the sidebar becomes a drawer: the header
toggle opens it, Escape or the backdrop closes it and returns focus. Replies are a local simulation — no model or
service is called.
Ready.
Recorded states
errorrecorded state; no setterclosedrecorded state; no setterstreamingrecorded state; no settercompletedrecorded state; no setter
Usage
Import
import '/components/chat/define.js';
// or register it yourself
import { DetentChat } from '/components/chat/chat.js';
Attributes
accent-coloractive-thread-idallow-attachmentsis-generatingmodel-badgeplaceholdersubtitletitle
Properties
messagemessagesonClearChatonNewChatonRetryonSelectThreadonSendMessagethreads
Slots
defaultempty-placeholder
Events
detent-send-message— {}detent-select-thread— {}detent-new-chat— {}detent-clear-chat— {}detent-retry— {}
Example
Runnable example: site/examples/chat.ts. The snippet below is generated from the catalogued attributes.
<detent-chat
accent-color
active-thread-id
allow-attachments
is-generating
model-badge
placeholder
subtitle
title
></detent-chat>
Property-only values
const element = document.querySelector('detent-chat');
if (element !== null) {
// messages: ChatMessageItem[]
element.messages = [];
// onSendMessage: (content: string, attachments?: ChatAttachment[]) => void
element.onSendMessage = () => {};
// threads: ChatConversationThread[]
element.threads = [];
// onSelectThread: (threadId: string) => void
element.onSelectThread = () => {};
// onNewChat: () => void
element.onNewChat = () => {};
// onClearChat: () => void
element.onClearChat = () => {};
}
Source
The pinned upstream source for this row:
src/components/ui/Chat.tsxsha256 de36dd7312c4…
Native files
The native module and its artifacts:
src/components/chat/chat.tssrc/components/chat/styles.csssrc/components/chat/define.tsdocs/components/chat.mdsite/examples/chat.tstests/browser/chat.spec.tsevidence/components/chat.md
API
Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.
Attributes
| Attribute | Property | Type | Default | Notes |
|---|---|---|---|---|
accent-color | — | — | — | Recorded in the catalog as an attribute. |
active-thread-id | activeThreadId | string | undefined | Kebab-case attribute; camelCase property. |
allow-attachments | — | — | — | Recorded in the catalog as an attribute. |
is-generating | isGenerating | boolean | false | Presence semantics: present means true, absent means false. |
model-badge | — | — | — | Recorded in the catalog as an attribute. |
placeholder | — | — | — | Recorded in the catalog as an attribute. |
subtitle | — | — | — | Recorded in the catalog as an attribute. |
title | — | — | — | Recorded in the catalog as an attribute. |
Properties
| Property | Type | Default | Notes |
|---|---|---|---|
message | — | — | Recorded in the catalog without a top-level type; pinned from source when the row is ported. |
messages | ChatMessageItem[] | [] | Property-only: assign from script; not accepted as an attribute. |
onClearChat | () => void | undefined | Property-only: assign from script; not accepted as an attribute. |
onNewChat | () => void | undefined | Property-only: assign from script; not accepted as an attribute. |
onRetry | — | — | Recorded in the catalog without a top-level type; pinned from source when the row is ported. |
onSelectThread | (threadId: string) => void | undefined | Property-only: assign from script; not accepted as an attribute. |
onSendMessage | (content: string, attachments?: ChatAttachment[]) => void | undefined | Property-only: assign from script; not accepted as an attribute. |
threads | ChatConversationThread[] | undefined | Property-only: assign from script; not accepted as an attribute. |
Events
| Event | Upstream prop | Payload type | Notes |
|---|---|---|---|
detent-send-message | onSendMessage | (content: string, attachments?: ChatAttachment[]) => void | Composed, bubbles; fires only for a user action. |
detent-select-thread | onSelectThread | (threadId: string) => void | Composed, bubbles; fires only for a user action. |
detent-new-chat | onNewChat | () => void | Composed, bubbles; fires only for a user action. |
detent-clear-chat | onClearChat | () => void | Composed, bubbles; fires only for a user action. |
detent-retry | onRetry | (messageId: string) => void | Composed, bubbles; fires only for a user action. |
Slots
| Slot | Notes |
|---|---|
default | Consumer content; rendered through a slot, never re-created. |
empty-placeholder | Consumer content; rendered through a slot, never re-created. |
CSS customisation
- `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
- React callback props become typed composed CustomEvents; programmatic state assignment emits none.
- Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- The ChatHeader, ChatSidebar, ChatMessage, ChatMessages and ChatComposer exports ship as documented parts of the one detent-chat tag (PC-2.1 keeps one tag per catalog row). useChat() becomes useChat(element), a live reader, because a custom element has no React context; it throws a TypeError for anything that is not an detent-chat element.
- message (one ChatMessageItem) is the native projection of the ChatMessage subcomponent and is appended after messages, so a streaming reply can be updated in place without rebuilding the transcript.
- activeThreadId is an attribute-backed _initial/controlled_ value, but a user click also updates the owned state and emits detent-select-thread (upstream is fully controlled and needs the consumer to re-render). The catalog records the controlled/uncontrolled mapping.
- Delivery status gets a visible text badge; upstream shows only a caret (streaming) and a failure line, so sending/sent/delivered were colour-only. This is a recorded accessibility correction (PC-12.6).
- Upstream's ad hoc fenced-code presentation inside assistant messages ( blocks and inline code splitting) is not ported: R-2.1 forbids a regex markdown renderer and R-2.3 keeps such source as text. Message content renders with white-space: pre-wrap, so fenced code is still readable as text.
- system messages get the author "System" instead of upstream's "Assistant" fallback, so the author line matches the recorded role.
- Framer Motion springs/AnimatePresence become CSS transitions (drawer slide, caret blink, spinner), all removed under prefers-reduced-motion: reduce.
- className is replaced by the documented parts and custom properties; there is no Tailwind in consumers.
What it does
A complete conversation surface: an optional grouped thread-history sidebar that becomes an off-canvas drawer on mobile, a transcript with role avatars, authors, timestamps, delivery badges, attachments and copy/retry actions, and an auto-growing composer with Enter-to-send, Shift+Enter-for-newline and file attachments.
Upstream behaviour
- Complete conversational layout with optional historical threads sidebar and responsive mobile drawer
- Rich message components supporting user, assistant, and system roles with timestamps and avatars
- Built-in file and image attachment upload system with chips, sizes, and removal actions
- Auto-expanding multiline composer with Enter-to-send and Shift+Enter for newline shortcuts
- Message delivery states: sending, sent, delivered, failed (with retry), and streaming
- Accessible action triggers for copying message content and managing conversation state
Differences from upstream
- `className` prop is replaced by documented CSS parts/custom properties; no Tailwind in consumers.
- React callback props become typed composed CustomEvents; programmatic state assignment emits none.
- Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- The ChatHeader, ChatSidebar, ChatMessage, ChatMessages and ChatComposer exports ship as documented parts of the one detent-chat tag (PC-2.1 keeps one tag per catalog row). useChat() becomes useChat(element), a live reader, because a custom element has no React context; it throws a TypeError for anything that is not an detent-chat element.
- message (one ChatMessageItem) is the native projection of the ChatMessage subcomponent and is appended after messages, so a streaming reply can be updated in place without rebuilding the transcript.
- activeThreadId is an attribute-backed _initial/controlled_ value, but a user click also updates the owned state and emits detent-select-thread (upstream is fully controlled and needs the consumer to re-render). The catalog records the controlled/uncontrolled mapping.
- Delivery status gets a visible text badge; upstream shows only a caret (streaming) and a failure line, so sending/sent/delivered were colour-only. This is a recorded accessibility correction (PC-12.6).
- Upstream's ad hoc fenced-code presentation inside assistant messages ( blocks and inline code splitting) is not ported: R-2.1 forbids a regex markdown renderer and R-2.3 keeps such source as text. Message content renders with white-space: pre-wrap, so fenced code is still readable as text.
- system messages get the author "System" instead of upstream's "Assistant" fallback, so the author line matches the recorded role.
- Framer Motion springs/AnimatePresence become CSS transitions (drawer slide, caret blink, spinner), all removed under prefers-reduced-motion: reduce.
- className is replaced by the documented parts and custom properties; there is no Tailwind in consumers.