# @kitn.ai/ui — Full Reference # @kitn.ai/ui > Framework-agnostic, Shadow-DOM web components for building AI chat interfaces — works in React, Vue, Angular, Svelte, or plain HTML. 78 custom elements, every one prefixed `kai-` (e.g. ``, ``): streaming responses, markdown + code rendering, reasoning/tool panels, attachments, conversation sidebar, voice input. Zero framework dependency for consumers; the SolidJS runtime it is authored in is bundled in, so the host needs nothing. ## Install ```bash npm install @kitn.ai/ui # SolidJS consumers also need the peer dep: npm install solid-js ``` ## #1 rule: array/object data goes on JS PROPERTIES, not HTML attributes This is the single most common mistake. Arrays and objects (`messages`, `models`, `context`, `suggestions`, `triggers`, …) MUST be assigned as JavaScript properties on the element. They CANNOT be passed as HTML attributes — an HTML attribute is always a string and will be ignored or mis-parsed. ```js const chat = document.querySelector('kai-chat'); chat.messages = [{ id: '1', role: 'assistant', content: 'Hi!' }]; // ✅ property ``` ```html ``` Only scalar values (string/number/boolean) work as attributes (e.g. `placeholder`, `loading`, `theme`). ## Two layers **Layer 1 — batteries-included web components** (`import '@kitn.ai/ui/elements'`): Drop an element into any framework (React, Vue, plain HTML). Data in via JS properties; interactions out via non-bubbling CustomEvents. - `` — full chat UI (message list + prompt input). The primary starting point. - `` — sidebar conversation browser with group support. - `` — standalone composer with send button. **Layer 2 — composable primitives** (`import { … } from '@kitn.ai/ui'`): All 78 elements are also exported individually. Use them for custom layouts or features `` does not expose (ChainOfThought, FeedbackBar, ThinkingBar, VoiceInput, …). Your bundler tree-shakes the rest. ## Key rules for the web components 1. **Array/object data = JS properties** (see above). Scalars may be attributes. 2. **Events are non-bubbling `CustomEvent`s** — listen directly on the element: `chat.addEventListener('kai-submit', (e) => console.log(e.detail.value))` 3. **`theme` attribute** (`'light' | 'dark' | 'auto'`) works on every element. Default `auto` follows `prefers-color-scheme`. 4. **Theming via CSS custom properties** — override `--kai-color-*` tokens on `:root`; they pierce Shadow DOM. ## ChatMessage schema (required for ``) ```ts interface ChatMessage { id: string; role: 'user' | 'assistant'; content: string; reasoning?: { text: string; label?: string }; tools?: ToolPart[]; attachments?: AttachmentData[]; actions?: ('copy' | 'like' | 'dislike' | 'regenerate' | 'edit')[]; } ``` ## Framework wiring **Plain HTML / CDN** ```html ``` **React** — typed wrappers auto-set properties and expose `on` props: ```tsx import { Chat } from '@kitn.ai/ui/react'; send(e.detail.value)} /> ``` **Vue** — use the element directly; pass arrays via `.prop`: ```vue ``` ## Theming ```css :root { --kai-color-background: #0f0f0f; --kai-color-primary: #7c3aed; --kai-color-muted: #1e1e1e; } ``` For plain HTML/CDN: ``. For Tailwind builds: `@import "@kitn.ai/ui/theme.css"` in your CSS. ## Docs - Full element reference (all 78 elements, every prop/event): ./llms-full.txt — https://kitn.dev/llms-full.txt - Machine-readable Custom Elements Manifest: https://unpkg.com/@kitn.ai/ui/dist/custom-elements.json - Working examples: https://github.com/kitn-ai/ui/tree/main/examples - Storybook: https://storybook.kitn.dev - Repository: https://github.com/kitn-ai/ui --- ## How to build a chat app in 5 steps ### 1 — Install ```bash npm install @kitn.ai/ui ``` ### 2 — Pick your layer Drop-in: use `` for a full chat UI in one tag (`import '@kitn.ai/ui/elements'`). Composable: combine ``, ``, ``, … in your own markup. ### 3 — Handle `submit` and stream ```js import '@kitn.ai/ui/elements'; const chat = document.querySelector('kai-chat'); chat.messages = []; chat.addEventListener('kai-submit', async (e) => { const userText = e.detail.value; // Append the user message (new array — see streaming note) const history = [...chat.messages, { id: crypto.randomUUID(), role: 'user', content: userText }]; chat.messages = history; chat.loading = true; // Add an empty assistant placeholder to stream into const aid = crypto.randomUUID(); chat.messages = [...history, { id: aid, role: 'assistant', content: '' }]; let answer = ''; for await (const token of streamFromYourAPI(history)) { answer += token; chat.messages = chat.messages.map((m) => (m.id === aid ? { ...m, content: answer } : m)); } chat.loading = false; }); ``` ### 4 — Wire optional features - Reasoning: add `reasoning: { text: '…' }` to an assistant message. - Tool calls: add `tools: [{ type: 'search', state: 'output-available', input: {…}, output: {…} }]`. - Model switcher: `chat.models = [{ id: 'gpt-4o', name: 'GPT-4o' }]; chat.currentModel = 'gpt-4o';` — listen for `modelchange`. - Token meter: `chat.context = { usedTokens: 1200, maxTokens: 128000 };`. - History sidebar: add ``; listen for `select` and `newchat`. ### 5 — Theme Override `--kai-color-*` tokens on `:root` (they pierce Shadow DOM). --- ## Streaming recipe (critical) To update messages while streaming, **reassign a NEW array containing a NEW message object** on every chunk. Mutating an existing message object in place will NOT trigger a re-render: ```js // ✅ re-renders chat.messages = chat.messages.map((m) => (m.id === id ? { ...m, content: next } : m)); // ❌ does NOT re-render chat.messages[i].content = next; ``` The same rule applies to every array/object property (`models`, `context`, `suggestions`, …): replace, don't mutate. --- ## Element reference (78 elements, generated from custom-elements.json) Every element also accepts the `theme` attribute. Array/object properties are marked with a `—` attribute: they must be set as JS properties. ### `kai-agent-card` / `AgentCard` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `name` | `name` | `undefined \| string` | The agent's name — the primary label. Attribute: `name`. | | `active` | `active` | `undefined \| false \| true` | Selected / focused state: highlighted border + surface. Attribute: `active`. | | `needsAttention` | `needs-attention` | `undefined \| false \| true` | Raise a prominent "Needs you" pill plus a glowing amber edge — the attention-routing signal that pulls focus to this agent. Attribute: `needs-attention`. | | `status` | — | `undefined \| { tone: "working" \| "idle" \| "done" \| "error" \| "blocked"; label?: undefined \| string; pulse?: undefined \| false \| true }` | Run status — a JS PROPERTY (object), not an attribute. Shape: `{ tone, label?, pulse? }`, where `tone` is one of `working` \| `idle` \| `done` \| `error` \| `blocked` (maps to the kit's tool hues), `label` is an optional short string beside the dot, and `pulse` animates the dot. Set it with `el.status = { tone: 'working', label: 'Working', pulse: true }`. | **Events** (non-bubbling `CustomEvent`s — listen directly on the element): | Event | `detail` type | Description | |---|---|---| | `kai-activate` | `CustomEvent` | The card was activated — clicked, or Enter / Space while focused. Promote this agent back to focus. | | `kai-menu` | `CustomEvent` | The trailing "..." kebab was clicked. The consumer opens its own menu; the card only surfaces the affordance (the click does not also activate the card). | **Styleable parts** (restyle from outside via `kai-agent-card::part(name)`): | Part | Description | |---|---| | `::part(status)` | The leading tone-colored status dot. — `kai-agent-card::part(status) { width: 0.625rem; height: 0.625rem }` | | `::part(menu)` | The trailing overflow ("...") menu button. — `kai-agent-card::part(menu) { opacity: 1 }` | --- ### `kai-artifact` / `Artifact` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `src` | `src` | `undefined \| string` | URL the preview iframe frames. Consumer-controlled. | | `files` | — | `{ path: string; url?: undefined \| string; code?: undefined \| string; language?: undefined \| string; type?: undefined \| "html" \| "pdf" \| "image" \| "other"; additions?: undefined \| number; deletions?: undefined \| number; status?: undefined \| "added" \| "modified" \| "deleted" \| "renamed" \| "untracked" }[]` | Files for the Code tab tree + each file's preview `url`. Set as a JS property (array). | | `tab` | `tab` | `undefined \| "preview" \| "code"` | Controlled active tab: `preview` or `code`. When set, the artifact follows it (re-asserted on change). Leave unset for an uncontrolled tab (see `defaultTab`). | | `defaultTab` | `default-tab` | `undefined \| "preview" \| "code"` | Uncontrolled INITIAL tab (used only when `tab` is unset). Default `preview`. Seeds the starting tab; the user can then switch freely without the consumer re-asserting a controlled `tab`. | | `activeFile` | `active-file` | `undefined \| string` | Selected file path — syncs the tree highlight, Code source, and preview. | | `sandbox` | `sandbox` | `undefined \| string` | iframe `sandbox` override. Secure default `allow-scripts allow-forms` (NOT `allow-same-origin`). | | `iframeTitle` | `iframe-title` | `undefined \| string` | Accessible title for the preview iframe. | | `maximized` | `maximized` | `undefined \| false \| true` | Reflects the artifact's own maximized view-state (usually driven by the protocol). | | `expandable` | `expandable` | `undefined \| false \| true` | Show the expand-to-fill button (OPT-IN). | | `openInTab` | `open-in-tab` | `undefined \| false \| true` | Show the open-in-new-tab button (OPT-IN). | | `noNav` | `no-nav` | `undefined \| false \| true` | Hide back/forward. | | `noReload` | `no-reload` | `undefined \| false \| true` | Hide reload. | | `noHome` | `no-home` | `undefined \| false \| true` | Hide home. | | `noPathField` | `no-path-field` | `undefined \| false \| true` | Hide the address field. | | `noTabs` | `no-tabs` | `undefined \| false \| true` | Hide the Preview\|Code toggle. | | `standalone` | `standalone` | `undefined \| false \| true` | Standalone chrome: rounded corners + border (else square, borderless in-panel). | | `readonlyPath` | `readonly-path` | `undefined \| false \| true` | Show the address but make it read-only (visible, nav-tracking, non-editable). | | `displayUrl` | `display-url` | `undefined \| string` | Friendly address shown in the path field instead of the real current url (read-only, non-navigable). Use when the framed url is not consumer-facing (e.g. a `data:` blob) so a clean address shows instead of leaking it. Scalar string: set as the `display-url` attribute or the `displayUrl` property. | **Events** (non-bubbling `CustomEvent`s — listen directly on the element): | Event | `detail` type | Description | |---|---|---| | `kai-file-select` | `CustomEvent<{ path: string }>` | Fired when a file is selected. `detail.path`. | | `kai-maximize-change` | `CustomEvent<{ maximized: false \| true }>` | Artifact's own maximize button toggled (consumer-observable; non-bubbling). | | `kai-navigate` | `CustomEvent<{ url: string }>` | Fired when the preview navigates. `detail.url` = the new location. | | `kai-tab-change` | `CustomEvent<{ tab: "preview" \| "code" }>` | Fired when the Preview\|Code tab changes. `detail.tab`. | --- ### `kai-attachments` / `Attachments` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `items` | — | `{ id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[]` | The attachments to render. Set as a JS property (array). | | `variant` | `variant` | `undefined \| "grid" \| "inline" \| "list"` | Layout: `grid` = visual tiles, `inline` = icon + label chips, `list` = rows. | | `hoverCard` | `hover-card` | `undefined \| false \| true` | Wrap each item in a hover card that previews its details. | | `removable` | `removable` | `undefined \| false \| true` | Show a remove button per item; clicking it fires a `kai-remove` event. | | `showMediaType` | `show-media-type` | `undefined \| false \| true` | Also show the media type beneath the filename (non-grid variants). | | `emptyText` | `empty-text` | `undefined \| string` | Text shown when `items` is empty. | **Events** (non-bubbling `CustomEvent`s — listen directly on the element): | Event | `detail` type | Description | |---|---|---| | `kai-remove` | `CustomEvent<{ id: string }>` | A remove button was clicked. | **Styleable parts** (restyle from outside via `kai-attachments::part(name)`): | Part | Description | |---|---| | `::part(preview)` | The image shown in an attachment’s hover-card preview. Bounded by default (max ~320×256, aspect preserved) so a large image never blows up the card — raise or lower the cap from outside. — `kai-attachments::part(preview) { max-width: 32rem; max-height: 24rem }` | --- ### `kai-avatar` / `Avatar` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `src` | `src` | `undefined \| string` | Image URL/data-URI. When absent, the `fallback` initials show instead. | | `alt` | `alt` | `undefined \| string` | Alt text for the image. Defaults to `fallback`. | | `fallback` | `fallback` | `undefined \| string` | Short text shown when there's no image — usually initials (e.g. "JD", "AI"). | | `size` | `size` | `undefined \| "sm" \| "md" \| "lg"` | Size token: `sm` \| `md` (default) \| `lg`. | _No events._ --- ### `kai-badge` / `Badge` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `variant` | `variant` | `undefined \| "default" \| "count" \| "citation"` | `default` (muted pill) · `count` (compact number badge) · `citation` (filled primary, for inline citation markers). Defaults to `default`. | _No events._ **Styleable parts** (restyle from outside via `kai-badge::part(name)`): | Part | Description | |---|---| | `::part(badge)` | The badge pill. Restyle its background, color, or shape; the `variant` prop (default/count/citation) sets the defaults. — `kai-badge::part(badge) { background: var(--color-primary); color: var(--color-primary-foreground) }` | --- ### `kai-button` / `Button` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `variant` | `variant` | `undefined \| "default" \| "subtle" \| "ghost" \| "outline" \| "destructive"` | Visual style. `default` (filled), `subtle` (muted text, hover tint — the toolbar icon look), `ghost` (transparent, hover fill), `outline`, or `destructive`. Defaults to `default`. | | `size` | `size` | `undefined \| "sm" \| "md" \| "lg" \| "icon" \| "icon-sm"` | Size token. `icon` / `icon-sm` are square (for icon-only buttons); `sm` / `md` / `lg` size text buttons. Defaults to `md`. | | `icon` | `icon` | `undefined \| string` | Leading icon: a named icon (e.g. `"mic"`, `"plus"`), an image URL/data-URI, or plain text. Renders before any slotted label. | | `iconTrailing` | `icon-trailing` | `undefined \| string` | Trailing icon, after the label (e.g. `"chevron-down"` for a menu affordance). | | `label` | `label` | `undefined \| string` | Accessible name. REQUIRED for icon-only buttons (no visible text); ignored when you slot visible text, which already names the button. | | `disabled` | `disabled` | `undefined \| false \| true` | Disable the button (non-interactive, dimmed). | | `full` | `full` | `undefined \| false \| true` | Stretch the button to the full width of its container (a block button) — e.g. a card CTA or a stacked action. Attribute: `full`. | | `align` | `align` | `undefined \| "start" \| "center" \| "end"` | Justify the button's content: `start`, `center` (default), or `end`. Combine with `full` for a full-width, left-aligned button. | | `type` | `type` | `undefined \| "button" \| "submit" \| "reset"` | Native button `type`. Defaults to `button` (so it never submits a form). | **Events** (non-bubbling `CustomEvent`s — listen directly on the element): | Event | `detail` type | Description | |---|---|---| | `kai-click` | `CustomEvent` | The button was activated (pointer or keyboard). Carries no detail. The native `click` also bubbles (composed) for consumers who prefer it. | **Slots** (project your own markup via `slot="name"` on a light-DOM child): | Slot | Mode | Description | |---|---|---| | `icon` | replace | A custom leading icon (any inline SVG, inherits `currentColor`). Wins over the `icon` prop. | **Styleable parts** (restyle from outside via `kai-button::part(name)`): | Part | Description | |---|---| | `::part(button)` | The button element. Restyle radius, padding, colors, or weight from outside; the `variant`/`size` props set the defaults. — `kai-button::part(button) { border-radius: 9999px; font-weight: 600 }` | --- ### `kai-card` / `Card` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `appearance` | `appearance` | `undefined \| "outlined" \| "filled" \| "plain" \| "accent"` | Surface treatment: `outlined` (default) \| `filled` \| `plain` \| `accent`. Attribute: `appearance`. | | `orientation` | `orientation` | `undefined \| "vertical" \| "horizontal" \| "responsive"` | `vertical` (default, media on top) \| `horizontal` (media at the start) \| `responsive` (horizontal when the card's container is wide enough, else vertical — a container query on the card's own width). Attribute: `orientation`. | | `collapse` | `collapse` | `undefined \| string` | The card width below which a `responsive` card collapses to vertical and the footer actions stack. A CSS length; default `28rem`. Attribute: `collapse`. | | `dense` | `dense` | `undefined \| false \| true` | Tighter spacing for dense lists. Attribute: `dense`. | | `dismissible` | `dismissible` | `undefined \| false \| true` | Show a close (×) that hides the card and emits `kai-dismiss`. Attribute: `dismissible`. Off by default. | | `href` | `href` | `undefined \| string` | Render the whole card as a link. Attribute: `href`. Wins over `clickable`. | | `target` | `target` | `undefined \| string` | `target` for the `href` anchor. Attribute: `target`. | | `rel` | `rel` | `undefined \| string` | `rel` for the `href` anchor. Attribute: `rel`. | | `clickable` | `clickable` | `undefined \| false \| true` | Make the whole card a button (`role="button"`, Enter/Space, hover affordance) that emits `kai-card-click`. Attribute: `clickable`. Ignored when `href` is set. | **Events** (non-bubbling `CustomEvent`s — listen directly on the element): | Event | `detail` type | Description | |---|---|---| | `kai-card-click` | `CustomEvent` | A `clickable`/`href` card was activated (click, or Enter/Space). | | `kai-dismiss` | `CustomEvent` | The card was dismissed via its × (it also hides itself). | **Slots** (project your own markup via `slot="name"` on a light-DOM child): | Slot | Mode | Description | |---|---|---| | `media` | inject | Full-bleed media (image/video/illustration) at the top (vertical) or start (horizontal). Clipped to the card corners. | | `header` | inject | Header content, e.g. a title. Rendered above the body. | | `header-actions` | inject | An actions cluster pinned to the end of the header row. | | `footer` | inject | Footer content rendered below the body. | | `footer-actions` | inject | Action buttons pinned to the end of the footer. Do NOT combine with a clickable/href card (nested interactive). | **Styleable parts** (restyle from outside via `kai-card::part(name)`): | Part | Description | |---|---| | `::part(card)` | The card root (a div, or an a when href is set). Restyle its radius, border, or background; set --kai-card-spacing for padding/gaps (the dense prop sets the compact default). — `kai-card::part(card) { border-radius: 1rem; --kai-card-spacing: 1.5rem }` | | `::part(media)` | The full-bleed media region. Cap or crop it from outside (e.g. a fixed height with object-fit). — `kai-card::part(media) { max-height: 12rem }` | | `::part(header)` | The header row (header content + header-actions). Add a divider or adjust its alignment. — `kai-card::part(header) { border-bottom: 1px solid var(--color-border) }` | | `::part(body)` | The default-slot body region. — `kai-card::part(body) { font-size: 0.9375rem }` | | `::part(footer)` | The footer row (footer content + footer-actions). — `kai-card::part(footer) { border-top: 1px solid var(--color-border) }` | | `::part(dismiss)` | The dismiss (×) button shown when dismissible. Recolor or reposition it from outside. — `kai-card::part(dismiss) { color: var(--color-muted-foreground) }` | --- ### `kai-cards` / `Cards` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `cards` | — | `undefined \| { type: string; id: string; data: unknown; title?: undefined \| string; resolution?: undefined \| { kind: "action"; action: string; payload?: unknown; at?: undefined \| string } \| { kind: "submit"; data: unknown; at?: undefined \| string } \| { kind: "dismissed"; at?: undefined \| string } \| { kind: "expired"; reason?: undefined \| string; at?: undefined \| string } }[]` | The stream of card envelopes to render. Set as a JS PROPERTY: `el.cards = [...]`. | | `types` | — | `undefined \| Record` | Optional type→tag overrides/additions (merged over the built-ins). Property: `el.types`. Typed as a plain string map (not the `CardTagMap` alias) so the generated React wrapper inlines it instead of emitting an unresolved named type. | | `policy` | — | `undefined \| { onSubmit?: undefined \| (cardId: string, data: unknown) => void; onAction?: undefined \| (cardId: string, action: string, payload?: unknown) => void; onSendPrompt?: undefined \| (text: string, opts: { mode: "compose" \| "send"; context?: unknown; }) => void; onOpen?: undefined \| (url: string, target: "tab" \| "artifact") => void; onState?: undefined \| (cardId: string, patch: unknown) => void; onDismiss?: undefined \| (cardId: string) => void; onReopen?: undefined \| (cardId: string) => void; onError?: undefined \| (cardId: string, message: string) => void; maxSendPromptMode?: undefined \| "compose" \| "send" }` | Optional CardPolicy handling child events. Property: `el.policy`. | **Events** (non-bubbling `CustomEvent`s — listen directly on the element): | Event | `detail` type | Description | |---|---|---| | `kai-card-resolved` | `CustomEvent<{ cardId: string; resolution: { kind: "action"; action: string; payload?: unknown; at?: undefined \| string } \| { kind: "submit"; data: unknown; at?: undefined \| string } \| { kind: "dismissed"; at?: undefined \| string } \| { kind: "expired"; reason?: undefined \| string; at?: undefined \| string } }>` | A child card transitioned to a resolved/deferred state (an action was chosen, a form/tasks submission landed, or it was dismissed) — re-emitted off the host as a non-bubbling convenience event so a consumer can observe resolution centrally without diffing the cards array. `detail` = `{ cardId, resolution }`. (A `reopen` un-resolves a card and has no `CardResolution`, so it does NOT fire this — observe reopen via the underlying bubbling `kai-card` event.) | --- ### `kai-chain-of-thought` / `ChainOfThought` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `steps` | — | `{ label: string; content?: undefined \| string; id?: undefined \| string }[]` | The reasoning steps. Set as a JS property. Compound sub-parts collapse to this one data model (Route 1). Each `{ label, content?, id? }`. | | `type` | `type` | `undefined \| "single" \| "multiple"` | Open mode: `'multiple'` (default — any number of steps open at once) or `'single'` (at most one open; opening a step closes the others). | | `value` | — | `undefined \| string \| string[]` | Controlled open step key(s). When set, it WINS over user interaction (the consumer owns the open set). String in `single` mode, string[] in `multiple` mode. Set as a JS property. | | `defaultValue` | — | `undefined \| string \| string[]` | Uncontrolled INITIAL open step key(s) — seeds which steps render expanded. Ignored once `value` is provided. Set as a JS property. | **Events** (non-bubbling `CustomEvent`s — listen directly on the element): | Event | `detail` type | Description | |---|---|---| | `kai-value-change` | `CustomEvent<{ value: string \| string[] }>` | The open set changed — by user click OR an expand()/collapse()/toggle() call. `value` is a string in `single` mode, a string[] in `multiple` mode. (Maps Radix Accordion's onValueChange.) | --- ### `kai-chat` / `Chat` **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes): | Property | Attribute | Type | Description | |---|---|---|---| | `messages` | — | `{ id: string; role: "user" \| "assistant"; content: string; reasoning?: undefined \| { text: string; label?: undefined \| string }; tools?: undefined \| { type: string; state: "input-streaming" \| "input-available" \| "output-available" \| "output-error"; input?: undefined \| Record; output?: undefined \| Record; toolCallId?: undefined \| string; errorText?: undefined \| string }[]; attachments?: undefined \| { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[]; actions?: undefined \| ("copy" \| "like" \| "dislike" \| "regenerate" \| "edit" \| { id: string; label: string; icon?: undefined \| string; tooltip?: undefined \| string })[]; avatar?: undefined \| { src?: undefined \| string; fallback?: undefined \| string; alt?: undefined \| string }; feedback?: undefined \| "like" \| "dislike" }[]` | The full message thread to render, newest last. Each entry carries its role, content, and optional reasoning/tools/attachments/actions. Set as a JS property (`el.messages = [...]`). | | `value` | — | `undefined \| string \| ({ type: "text"; text: string } \| { type: "entity"; entity: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record } })[]` | Value of the input. A **string** is controlled (the host owns the text and updates it on `kai-value-change`). A **ComposerDoc** is a one-time seed that pre-populates pills; the user then edits freely. Leave unset for uncontrolled. | | `placeholder` | `placeholder` | `undefined \| string` | Placeholder text shown in the empty input. | | `loading` | `loading` | `undefined \| false \| true` | When true, shows the loading/streaming state and disables submit (use while awaiting the assistant's reply). | | `suggestions` | — | `undefined \| string[]` | Starter prompts shown above the input when the thread is empty. Clicking one follows `suggestionMode`. Set as a JS property. | | `suggestionMode` | `suggestion-mode` | `undefined \| "submit" \| "fill"` | What clicking a suggestion does: `'submit'` (default) sends it immediately as if typed and submitted; `'fill'` just places it in the input. | | `persistSuggestions` | `persist-suggestions` | `undefined \| false \| true` | Keep suggestions visible after the conversation starts. By default suggestions are conversation starters and hide once `messages` is non-empty; set this to keep them always shown. Default false. | | `proseSize` | `prose-size` | `undefined \| "sm" \| "lg" \| "xs" \| "base"` | Body/prose font scale for rendered markdown (`'xs' \| 'sm' \| 'base' \| 'lg'`). Defaults to `'sm'`. | | `codeTheme` | `code-theme` | `undefined \| string` | Shiki theme name for syntax-highlighted code blocks (e.g. `'github-dark-dimmed'`). | | `codeHighlight` | `code-highlight` | `undefined \| false \| true` | Enable Shiki syntax highlighting in code blocks. Turn off to render plain `
` blocks (lighter, no highlighter load). Default true. |
| `chatTitle` | `chat-title` | `undefined \| string` | Optional header title shown on the left of the header. |
| `models` | — | `undefined \| { id: string; name: string; provider?: undefined \| string; description?: undefined \| string; group?: undefined \| string }[]` | Optional model list. When set (>1 model) a ModelSwitcher is shown in the header and a `kai-model-change` event fires on selection. |
| `currentModel` | `current-model` | `undefined \| string` | The currently selected model id (pairs with `models`). |
| `context` | — | `undefined \| { usedTokens: number; maxTokens: number; inputTokens?: undefined \| number; outputTokens?: undefined \| number; estimatedCost?: undefined \| number }` | Optional context-window token usage. When set, a Context token meter is shown in the header. |
| `scrollButton` | `scroll-button` | `undefined \| false \| true` | Show the scroll-to-bottom button inside the scroll area. Default true. |
| `headerStart` | `header-start` | `undefined \| false \| true` | Whether the host has `slot="header-start"` content (left of the title) — set by the `` facade so a custom control forces the header open. |
| `headerEnd` | `header-end` | `undefined \| false \| true` | Whether the host has `slot="header-end"` content (right of the controls). |
| `headerFull` | `header-full` | `undefined \| false \| true` | REPLACE — full custom header in place of the built-in title/model/context bar. |
| `sidebar` | `sidebar` | `undefined \| false \| true` | INJECT — left sidebar column (e.g. a conversation list / your own nav). |
| `empty` | `empty` | `undefined \| false \| true` | REPLACE — custom zero-state rendered in the message area while the thread is empty (replaces the empty message list only; the composer and its suggestions still render). |
| `composer` | `composer` | `undefined \| false \| true` | REPLACE — full custom composer in place of the built-in prompt input. The projected content wires its own submit (the data-flow boundary). |
| `composerActions` | `composer-actions` | `undefined \| false \| true` | INJECT — accessory row just above the composer (e.g. extra actions). |
| `footer` | `footer` | `undefined \| false \| true` | INJECT — footer row below the composer (disclaimers, token meter, …). |
| `search` | `search` | `undefined \| false \| true` | Show a Search (Globe) button in the input toolbar; fires a `search` event. |
| `voice` | `voice` | `undefined \| false \| true` | Show a Voice (Mic) button in the input toolbar; fires a `voice` event. |
| `triggers` | — | `undefined \| { char: string; kind: string; items?: undefined \| { id: string; label: string; icon?: undefined \| string; description?: undefined \| string; group?: undefined \| string; kind?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record }[] }[]` | Rich entity triggers — each `{ char, kind, items }` opens a caret-anchored menu that inserts an atomic pill (`/` skills, `@` agents/plugins). Set as a JS property; forwarded to the input. |
| `kindIcons` | — | `undefined \| Record` | Default icon per entity kind (kind → image src) for pills/menu items. |
| `actionsReveal` | `actions-reveal` | `undefined \| "always" \| "hover"` | Whether each message's action bar is always visible (`'always'`, default) or only revealed on hover of that message row (`'hover'`). |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-attachments-change` | `CustomEvent<{ attachments: { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[] }>` | The staged attachments changed (file added or removed). Carries the full current list so a consumer can react in real time. |
| `kai-message-action` | `CustomEvent<{ messageId: string; action: string; state?: undefined \| "on" \| "off" }>` | An action button on a message was clicked. `action` is the built-in name or custom id. `state` is present only for the toggleable feedback votes: `'on'` when a like/dislike is set, `'off'` when re-tapped to clear. |
| `kai-model-change` | `CustomEvent<{ modelId: string }>` | The header model switcher changed. |
| `kai-search` | `CustomEvent>` | The Search button was clicked. |
| `kai-submit` | `CustomEvent<{ value: string; attachments: { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[] }>` | User submitted a message. |
| `kai-suggestion-click` | `CustomEvent<{ value: string }>` | A suggestion chip was clicked (only in `suggestion-mode="fill"`). |
| `kai-value-change` | `CustomEvent<{ value: string }>` | Fired on every input change. |
| `kai-voice` | `CustomEvent>` | The Mic / voice button was clicked. |

**Slots** (project your own markup via `slot="name"` on a light-DOM child):

| Slot | Mode | Description |
|---|---|---|
| `header-start` | inject | Leading header controls, left of the title. |
| `header-end` | inject | Trailing header controls. |
| `header` | replace | Full custom header; replaces the built-in title/model/context bar. |
| `sidebar` | inject | Left column (your nav / conversation list). Fixed width; use compose-your-own for resizable. |
| `empty` | replace | Custom zero-state rendered in the message area while the thread is empty. Replaces the empty message list only — the composer and any suggestions still render. |
| `composer` | replace | Full custom composer; you own submit + loading, drive the thread via messages. |
| `composer-actions` | inject | Accessory row above the composer. |
| `footer` | inject | Row below the composer (disclaimers, token meter). |

**Styleable parts** (restyle from outside via `kai-chat::part(name)`):

| Part | Description |
|---|---|
| `::part(header-bar)` | The built-in header bar (the title / model-switcher / context row that hosts the header-start/header-end inject slots). Restyle its height, padding, or gap from outside without replacing the whole header via the `header` slot. — `kai-chat::part(header-bar) { height: 3.5rem; padding-inline: 1rem; gap: 0.5rem }` |
| `::part(header)` | Full custom header; replaces the built-in title/model/context bar. |
| `::part(sidebar)` | Left column (your nav / conversation list). Fixed width; use compose-your-own for resizable. |
| `::part(footer)` | Row below the composer (disclaimers, token meter). |

---

### `kai-checkpoint` / `Checkpoint`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `label` | `label` | `undefined \| string` | Optional text beside the icon. |
| `tooltip` | `tooltip` | `undefined \| string` | Tooltip on hover. |
| `variant` | `variant` | `undefined \| "default" \| "ghost" \| "outline"` | Visual button style. |
| `size` | `size` | `undefined \| "sm" \| "md" \| "lg" \| "icon" \| "icon-sm"` | Button size (use an `icon*` size for an icon-only checkpoint). |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-select` | `CustomEvent` | The checkpoint was clicked. |

---

### `kai-choice` / `Choice`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `data` | — | `undefined \| Record` | The choice definition (the CardEnvelope.data). Set as a JS PROPERTY: `el.data = { prompt, options:[…], allowOther?, submitLabel? }`. Import `ChoiceCardData` from `@kitn.ai/ui` for the full shape. |
| `cardId` | `card-id` | `undefined \| string` | Stable card id correlating every emitted CardEvent. Attribute: `card-id`. |
| `heading` | `heading` | `undefined \| string` | Heading rendered in the card chrome (= CardEnvelope.title). Attribute: `heading`. |
| `resolution` | — | `undefined \| Record` | Set when the user resolved this card; renders the read-only view. Property: `el.resolution = { kind:'action', action:'…' }`. |
| `value` | `value` | `undefined \| string` | Controlled selection — the selected option id. When set, the consumer owns the current pick (RadioGroup `value`). Attribute: `value`. |
| `defaultValue` | `default-value` | `undefined \| string` | Option id to pre-select on mount (uncontrolled seed). Attribute: `default-value`. |
| `disabled` | `disabled` | `undefined \| false \| true` | Disable the whole radiogroup + Submit (e.g. while the agent is busy). Attribute: `disabled`. |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-value-change` | `CustomEvent<{ value: string }>` | The selection changed BEFORE submit (a row click or the `select()` method). Distinct from the terminal `action` verb on the `kai-card` contract event. |

---

### `kai-coachmark` / `Coachmark`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `open` | `open` | `undefined \| false \| true` | Drive/observe open state (Shoelace-style: settable + reflected to the `open` attribute; the element still self-manages). Set `el.open = true`, or ``; listen for `kai-open-change`. |
| `defaultOpen` | `default-open` | `undefined \| false \| true` | Initial open state on mount (uncontrolled seed). |
| `headline` | `headline` | `undefined \| string` | The bold title. Named `headline` because `title` collides with the global `HTMLElement.title` attribute (it throws at registration). |
| `badge` | `badge` | `undefined \| string` | A small badge pill beside the headline (e.g. "New"). |
| `placement` | `placement` | `undefined \| string` | Floating placement relative to the anchor (default `bottom`). |
| `tone` | `tone` | `undefined \| "error" \| "primary" \| "info" \| "success" \| "warning"` | Color tone: `primary` (default, theme accent), `info` (blue), `success` (green), `warning` (amber), or `error` (red) — reusing the kit's tool hues. |
| `arrow` | `arrow` | `undefined \| false \| true` | Render the arrow that points at the anchor (default `true`). Set `arrow="false"` for a plain bubble with no pointer. |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-dismiss` | `CustomEvent>` | The × dismiss button was pressed. The consumer records that this hint was seen so it won't show again. |
| `kai-open-change` | `CustomEvent<{ open: false \| true }>` | The coachmark opened or closed (a method, the ×, or a driven `open`). |

**Slots** (project your own markup via `slot="name"` on a light-DOM child):

| Slot | Mode | Description |
|---|---|---|
| `content` | replace | The bubble body text shown under the headline. |

**Styleable parts** (restyle from outside via `kai-coachmark::part(name)`):

| Part | Description |
|---|---|
| `::part(bubble)` | The hint bubble panel. Restyle its background, radius, or padding from outside; the default is bg-primary. — `kai-coachmark::part(bubble) { border-radius: 1rem }` |
| `::part(arrow)` | The arrow pointing at the anchor. Inherits the bubble color; recolor it alongside the bubble. — `kai-coachmark::part(arrow) { background: var(--color-accent) }` |
| `::part(badge)` | The small badge pill beside the headline (e.g. "New"). — `kai-coachmark::part(badge) { text-transform: none }` |
| `::part(title)` | The bold headline text. — `kai-coachmark::part(title) { font-size: 0.9375rem }` |
| `::part(dismiss)` | The dismiss button. Recolor or reposition it from outside. — `kai-coachmark::part(dismiss) { color: var(--color-primary-foreground) }` |

---

### `kai-code-block` / `CodeBlock`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `code` | `code` | `string` | The source code to render. |
| `language` | `language` | `undefined \| string` | Language grammar (e.g. `js`, `python`). Defaults to `tsx`. |
| `codeTheme` | `code-theme` | `undefined \| string` | Shiki theme name. |
| `codeHighlight` | `code-highlight` | `undefined \| false \| true` | Disable syntax highlighting (renders plain text, no Shiki). |
| `proseSize` | `prose-size` | `undefined \| "sm" \| "lg" \| "xs" \| "base"` | Code text sizing. |

_No events._

---

### `kai-command` / `Command`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `items` | — | `undefined \| { id: string; label: string; icon?: undefined \| string; description?: undefined \| string; shortcut?: undefined \| string; group?: undefined \| string }[]` | Flat list of items. Set as a JS property — not an HTML attribute. |
| `placeholder` | `placeholder` | `undefined \| string` | Placeholder text for the search input. |
| `emptyLabel` | `empty-label` | `undefined \| string` | Label shown when no items match the current query. |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-active-change` | `CustomEvent<{ id: undefined \| string }>` | Fired when the highlighted/active item changes — via Arrow keys or when filtering re-clamps the active row. `id` is the newly active item's id, or `undefined` when no item is active (e.g. the filtered list is empty). Lets a host preview the active item without committing a selection. |
| `kai-query-change` | `CustomEvent<{ value: string }>` | Fired on every keystroke in the search input. |
| `kai-select` | `CustomEvent<{ id: string }>` | Fired when the user selects an item (click or Enter). |

**Styleable parts** (restyle from outside via `kai-command::part(name)`):

| Part | Description |
|---|---|
| `::part(shortcut)` | The right-aligned per-row keyboard shortcut, rendered as kai-kbd key caps. Shown only when a row carries a `shortcut`. — `kai-command::part(shortcut) { opacity: 0.8 }` |

---

### `kai-compare` / `Compare`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `data` | — | `undefined \| Record` | The compare definition (prompt + the two candidates). Set as a JS PROPERTY: `el.data = { prompt, candidates: [A, B], collapse? }`. Import `ResponseCompareData` from `@kitn.ai/ui` for the full shape. |
| `compareId` | `compare-id` | `undefined \| string` | Stable id correlating every emitted event. Attribute: `compare-id`. |
| `selection` | — | `undefined \| Record` | Re-hydrate / control the user's pick. Set as a JS PROPERTY: `el.selection = { chosenId, rejectedIds }`. Renders the collapsed winner. |
| `layout` | `layout` | `undefined \| "auto" \| "columns" \| "tabs"` | Layout: `'auto'` (default — columns when wide, tabs when narrow, by CONTAINER width) \| `'columns'` (side-by-side) \| `'tabs'` (pills to switch). Attribute: `layout`. |
| `proseSize` | `prose-size` | `undefined \| "sm" \| "lg" \| "xs" \| "base"` | Prose/text size for the rendered candidates. Attribute: `prose-size`. |
| `codeTheme` | `code-theme` | `undefined \| string` | Shiki theme for code blocks in the candidates. Attribute: `code-theme`. |
| `codeHighlight` | `code-highlight` | `undefined \| false \| true` | Whether code blocks are syntax-highlighted. Attribute: `code-highlight`. |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-compare-select` | `CustomEvent<{ chosenId: string; rejectedIds: string[]; at?: undefined \| number }>` | The user committed a pick. `detail` = `{ chosenId, rejectedIds, at }`. |
| `kai-error` | `CustomEvent<{ compareId: string; message: string }>` | The definition was unusable. |
| `kai-ready` | `CustomEvent<{ compareId: string }>` | Both candidates have settled and the pick is live. |

---

### `kai-composer` / `Composer`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `value` | — | `undefined \| string \| ({ type: "text"; text: string } \| { type: "entity"; entity: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record } })[]` | Controlled value — string or a full ComposerDoc (set as JS property). |
| `placeholder` | `placeholder` | `undefined \| string` | Placeholder text shown when the composer is empty. |
| `disabled` | `disabled` | `undefined \| false \| true` | Disable the composer entirely (non-interactive). |
| `loading` | `loading` | `undefined \| false \| true` | Show a loading/streaming state and block submit. |
| `maxHeight` | `max-height` | `undefined \| string \| number` | Maximum height in px before the content scrolls. Default 240. |
| `submitOnEnter` | `submit-on-enter` | `undefined \| false \| true` | Whether pressing Enter (without Shift) submits. Default true. |
| `triggers` | — | `undefined \| { char: string; kind: string; items?: undefined \| { id: string; label: string; icon?: undefined \| string; description?: undefined \| string; group?: undefined \| string; kind?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record }[] }[]` | Trigger definitions — set as a JS property. |
| `highlights` | — | `undefined \| (string \| { pattern: string; flags?: undefined \| string; class?: undefined \| string })[]` | Keyword highlight rules — set as a JS property. |
| `kindIcons` | — | `undefined \| Record` | Default icon per entity kind (kind → image URL/data-URI) for items without their own `icon`. Overrides the built-in agent/plugin glyphs. JS property. |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-blur` | `CustomEvent<{ originalEvent: FocusEvent }>` | The composer lost focus. |
| `kai-entity-add` | `CustomEvent<{ entity: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record } }>` | An entity pill was inserted into the composer. |
| `kai-entity-remove` | `CustomEvent<{ entity: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record } }>` | An entity pill was deleted from the composer. |
| `kai-focus` | `CustomEvent<{ originalEvent: FocusEvent }>` | The composer gained focus. `focus`/`blur` are NOT composed natively, so they don't escape the shadow root — these re-expose them on the host. (For `keydown`/`paste`/`focusin`/`focusout`, listen NATIVELY on ``: they're composed and already cross the shadow boundary.) |
| `kai-submit` | `CustomEvent<{ doc: ({ type: "text"; text: string } \| { type: "entity"; entity: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record } })[]; text: string; entities: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record }[] }>` | The user submitted the composer (Enter or programmatic submit). |
| `kai-trigger` | `CustomEvent<{ char: string; query: string; rect: DOMRect }>` | A trigger character was detected at the caret (e.g. `/` or `@`). |
| `kai-trigger-close` | `CustomEvent>` | The active trigger was dismissed (Escape, space, or outside click). |
| `kai-value-change` | `CustomEvent<{ doc: ({ type: "text"; text: string } \| { type: "entity"; entity: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record } })[]; text: string; entities: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record }[] }>` | The content changed (fires on every input event). |

---

### `kai-confirm` / `Confirm`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `data` | — | `undefined \| Record` | The confirm definition (the CardEnvelope.data). Set as a JS PROPERTY: `el.data = { body, tone, actions:[…] }`. Import `ConfirmCardData` from `@kitn.ai/ui` for the full shape. |
| `cardId` | `card-id` | `undefined \| string` | Stable card id correlating every emitted CardEvent. Attribute: `card-id`. |
| `heading` | `heading` | `undefined \| string` | Heading rendered in the card chrome (= CardEnvelope.title). Attribute: `heading`. |
| `autofocus` | `autofocus` | `undefined \| false \| true` | Focus the default action on mount (off by default — no focus-stealing). Attribute: `autofocus`. |
| `resolution` | — | `undefined \| Record` | Set when the user resolved this card; renders the read-only view. Property: `el.resolution = { kind:'action', action:'…' }`. |

_No events._

---

### `kai-context` / `Context`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `context` | — | `undefined \| { usedTokens: number; maxTokens: number; inputTokens?: undefined \| number; outputTokens?: undefined \| number; reasoningTokens?: undefined \| number; cacheTokens?: undefined \| number; estimatedCost?: undefined \| number }` | Token-usage data. Set as a JS property. |
| `warnThreshold` | `warn-threshold` | `undefined \| number` | Fraction (0–1) above which the meter turns yellow. Defaults to `0.7` (70%). |
| `dangerThreshold` | `danger-threshold` | `undefined \| number` | Fraction (0–1) above which the meter turns red. Defaults to `0.9` (90%). |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-threshold-change` | `CustomEvent<{ level: "ok" \| "warn" \| "danger" }>` | Fires when the computed severity level changes (ok → warn → danger or back). `detail.level` is `'ok'`, `'warn'`, or `'danger'`. |

---

### `kai-conversations` / `Conversations`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `groups` | — | `{ id: string; userId?: undefined \| string; teamId?: undefined \| string; name: string; sortOrder: number; createdAt: string }[]` | Pre-bucketed conversation groups (e.g. "Today", "Yesterday"), each with its own conversations. Use this when you want to control the grouping/headers yourself; otherwise pass a flat `conversations` array. Set as a JS property. |
| `conversations` | — | `{ id: string; title: string; groupId?: undefined \| string; scope: { type: "document" \| "collection"; documentId?: undefined \| string; filters?: undefined \| { tags?: undefined \| string[]; authors?: undefined \| string[]; contentType?: undefined \| "transcript" \| "markdown"; dateRange?: undefined \| { from: string; to: string } } }; messageCount: number; lastMessageAt: string; updatedAt: string; trailing?: undefined \| string }[]` | A flat list of conversation summaries; the component buckets them by recency for you. Ignored when `groups` is provided. Set as a JS property. |
| `activeId` | `active-id` | `undefined \| string` | The id of the currently-open conversation, highlighted in the list. |
| `collapsed` | `collapsed` | `undefined \| false \| true` | Controlled collapsed state. Set as a JS property (`el.collapsed = true`) to drive the rail from your app, updating it in response to `kai-collapse-toggle`. Omit for uncontrolled (the element manages it). Collapsed shrinks the rail to a floating reopen button. |
| `defaultCollapsed` | `default-collapsed` | `undefined \| false \| true` | Initial collapsed state when uncontrolled (default false). Use the `default-collapsed` attribute to start collapsed in plain HTML. |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-collapse-toggle` | `CustomEvent<{ collapsed: false \| true }>` | The rail was collapsed or expanded (via the toggle, the reopen button, or a `collapse()`/`expand()`/`toggle()` call). |
| `kai-conversation-select` | `CustomEvent<{ id: string }>` | A conversation was selected. |
| `kai-new-chat` | `CustomEvent>` | The "New chat" button was clicked. |
| `kai-search` | `CustomEvent<{ query: string }>` | The built-in search box query changed (typing, or a programmatic `clear()` which fires it with `''`). Lets a consumer mirror or server-side the filter. |
| `kai-toggle-sidebar` | `CustomEvent>` | The sidebar toggle was clicked. |

**Slots** (project your own markup via `slot="name"` on a light-DOM child):

| Slot | Mode | Description |
|---|---|---|
| `header` | replace | Full custom title bar; replaces the built-in toggle / "Chats" / New-chat row. |
| `empty` | replace | Custom zero-state shown when there are no conversations; replaces the built-in "No conversations yet". |
| `footer` | inject | A row below the list — account, settings, or usage. |

**Styleable parts** (restyle from outside via `kai-conversations::part(name)`):

| Part | Description |
|---|---|
| `::part(trailing)` | The right-aligned trailing text on each conversation row (a count, status, or relative time). Set it per item via the `trailing` field; otherwise a short auto relative time is derived from `updatedAt`. Recolor or resize it from outside. — `kai-conversations::part(trailing) { color: var(--color-primary); font-variant-numeric: tabular-nums }` |

---

### `kai-dialog` / `Dialog`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `open` | `open` | `undefined \| false \| true` | Drive/observe open state (Shoelace-style: settable + reflected to the `open` attribute; the element still self-manages on Escape/backdrop). Set `el.open = true`, or ``; listen for `kai-open-change`. |
| `defaultOpen` | `default-open` | `undefined \| false \| true` | Initial open state on mount (uncontrolled seed). |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-open-change` | `CustomEvent<{ open: false \| true }>` | The dialog opened or closed (Escape, backdrop click, a driven `open`, or a method). |

**Slots** (project your own markup via `slot="name"` on a light-DOM child):

| Slot | Mode | Description |
|---|---|---|
| `header` | inject | Optional title region at the top of the panel. |
| `footer` | inject | Optional actions region at the bottom of the panel. |

**Styleable parts** (restyle from outside via `kai-dialog::part(name)`):

| Part | Description |
|---|---|
| `::part(backdrop)` | The full-area scrim behind the panel. Restyle its color/blur. — `kai-dialog::part(backdrop) { background: rgb(0 0 0 / 0.6) }` |
| `::part(panel)` | The centered modal panel. Restyle width, radius, padding. — `kai-dialog::part(panel) { max-width: 32rem }` |
| `::part(body)` | The scrolling content region (the default slot). — `kai-dialog::part(body) { padding: 1.25rem }` |
| `::part(header)` | Optional title region at the top of the panel. |
| `::part(footer)` | Optional actions region at the bottom of the panel. |

---

### `kai-editable-label` / `EditableLabel`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `value` | `value` | `undefined \| string` | The label text — settable and reflected to the `value` attribute. Read `el.value` for live state. |
| `editing` | `editing` | `undefined \| false \| true` | Controlled edit state. `el.editing = true` opens the field; reflected to the `editing` attribute. |
| `placeholder` | `placeholder` | `undefined \| string` | Placeholder shown while editing / when the value is empty. |
| `disabled` | `disabled` | `undefined \| false \| true` | Disable entering edit mode. |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-cancel` | `CustomEvent>` | Edit was cancelled (Esc); the text is restored. |
| `kai-rename` | `CustomEvent<{ value: string }>` | Committed a changed value (Enter / blur). |

**Styleable parts** (restyle from outside via `kai-editable-label::part(name)`):

| Part | Description |
|---|---|
| `::part(text)` | The read-mode label text. Restyle its typography; it swaps to the input on edit. — `kai-editable-label::part(text) { font-weight: 600 }` |
| `::part(input)` | The edit-mode input (the composed kai-input field). — `kai-editable-label::part(input) { font: inherit }` |

---

### `kai-embed` / `Embed`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `cardId` | `card-id` | `undefined \| string` | Stable card id correlating every emitted event. Set as an attribute or property. |
| `data` | — | `undefined \| { provider: "youtube" \| "vimeo" \| "generic"; id?: undefined \| string; url?: undefined \| string; title?: undefined \| string; poster?: undefined \| string; start?: undefined \| number; aspectRatio?: undefined \| "16:9" \| "4:3" \| "1:1" \| "9:16" }` | The embed payload (provider + id/url + options). Set as a JS **property** (object). |

_No events._

---

### `kai-empty` / `Empty`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `emptyTitle` | `empty-title` | `undefined \| string` | Title text. Attribute: `empty-title` (`title` is a global HTML attribute). |
| `description` | `description` | `undefined \| string` | Description text. |

_No events._

---

### `kai-feedback-bar` / `FeedbackBar`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `barTitle` | `bar-title` | `undefined \| string` | The banner label (e.g. "Was this helpful?"). Attribute: `bar-title` (`title` is avoided — it's a global HTML attribute). |
| `collectDetail` | `collect-detail` | `undefined \| false \| true` | When set, a not-helpful vote opens an optional detail form before the thank-you confirmation. Attribute: `collect-detail`. |
| `categories` | — | `undefined \| string[]` | Optional category chips for the detail form. Set as a JS property (array). |
| `detailTitle` | `detail-title` | `undefined \| string` | Heading for the detail form. Attribute: `detail-title`. |
| `detailPlaceholder` | `detail-placeholder` | `undefined \| string` | Placeholder for the detail comment box. Attribute: `detail-placeholder`. |
| `submitLabel` | `submit-label` | `undefined \| string` | Submit button label in the detail form. Attribute: `submit-label`. |
| `thanksMessage` | `thanks-message` | `undefined \| string` | Confirmation copy shown after a vote/submit. Attribute: `thanks-message`. |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-close` | `CustomEvent` | The user dismissed the banner. |
| `kai-feedback` | `CustomEvent<{ value: "helpful" \| "not-helpful" }>` | The user rated the response. `value` is `'helpful'` or `'not-helpful'`. |
| `kai-feedback-detail` | `CustomEvent<{ value: "helpful" \| "not-helpful"; category?: undefined \| string; comment?: undefined \| string }>` | The user submitted the optional detail form (`collect-detail`). |

---

### `kai-file-tree` / `FileTree`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `files` | — | `{ path: string; url?: undefined \| string; code?: undefined \| string; language?: undefined \| string; type?: undefined \| "html" \| "pdf" \| "image" \| "other"; additions?: undefined \| number; deletions?: undefined \| number; status?: undefined \| "added" \| "modified" \| "deleted" \| "renamed" \| "untracked" }[]` | The files to render. Set as a JS property (array of `{ path, url?, code?, language?, type?, additions?, deletions?, status? }`). |
| `activeFile` | `active-file` | `undefined \| string` | Selected file path — highlighted in the tree. |
| `defaultExpanded` | — | `undefined \| string[]` | Folder paths expanded initially. Omit to start with all folders open. |
| `summary` | `summary` | `undefined \| false \| true` | Show a changed-files summary header (file count + summed `+/-` + Collapse-all). Attribute: `summary`. Off by default. |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-select` | `CustomEvent<{ path: string }>` | Fired when a file is selected. `detail.path` = the file's path. |

**Styleable parts** (restyle from outside via `kai-file-tree::part(name)`):

| Part | Description |
|---|---|
| `::part(summary)` | The changed-files summary header (the file count, the summed +additions/-deletions, and the Collapse-all/Expand-all toggle). Rendered only when the `summary` attribute is set; restyle or hide it from outside. — `kai-file-tree::part(summary) { border-bottom: none; padding-block: 0.5rem }` |
| `::part(status)` | The per-row change-status letter (A/M/D/R/U), shown when a file carries a `status`. Colored with the conventional VCS tool hues; restyle from outside. — `kai-file-tree::part(status) { font-weight: 700 }` |
| `::part(stat-additions)` | The trailing `+N` additions stat on a file row (success/green tool hue, tabular-nums). Shown only when a file carries `additions`. — `kai-file-tree::part(stat-additions) { color: var(--color-tool-green) }` |
| `::part(stat-deletions)` | The trailing `-N` deletions stat on a file row (error/red tool hue, tabular-nums). Shown only when a file carries `deletions`. — `kai-file-tree::part(stat-deletions) { color: var(--color-tool-red) }` |

---

### `kai-file-upload` / `FileUpload`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `multiple` | `multiple` | `undefined \| false \| true` | Allow selecting multiple files (default true). |
| `accept` | `accept` | `undefined \| string` | `accept` attribute for the file picker (e.g. `image/*`). |
| `disabled` | `disabled` | `undefined \| false \| true` | Disable the dropzone — no clicking, no drag-and-drop. |
| `label` | `label` | `undefined \| string` | Default dropzone label (overridable via the default slot). |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-files-added` | `CustomEvent<{ files: File[] }>` | Files were picked or dropped. |

---

### `kai-form` / `Form`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `data` | — | `undefined \| Record` | The form definition — a JSON Schema (`type:'object'`) + `x-kai-*` UI hints (the CardEnvelope.data). Set as a JS PROPERTY: `el.data = { type:'object', properties:{…} }`. Import the `FormDefinition` type from `@kitn.ai/ui` for the full shape (it is self-referential, so the element types it loosely). |
| `cardId` | `card-id` | `undefined \| string` | Stable card id correlating every emitted CardEvent. Attribute: `card-id`. |
| `heading` | `heading` | `undefined \| string` | Heading rendered in the card chrome (= CardEnvelope.title). Attribute: `heading`. |
| `resolution` | — | `undefined \| Record` | Set when the user resolved this card; renders the read-only view. Property: `el.resolution = { kind:'submit', data:{…} }`. |
| `values` | — | `undefined \| Record` | Controlled field values (JS property). When set, it wins over local edits. |
| `defaultValues` | — | `undefined \| Record` | Initial values overlaying the schema defaults (uncontrolled seed; JS property). |
| `disabled` | `disabled` | `undefined \| false \| true` | Disable all fields + submit. Attribute: `disabled`. |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-values-change` | `CustomEvent<{ values: Record; valid: false \| true }>` | The form's values changed on input — current coerced values + validity. |

---

### `kai-hover-card` / `HoverCard`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `openDelay` | `open-delay` | `undefined \| number` | Delay (ms) before the card opens on hover. Defaults to 0 (focus opens it immediately too). |
| `closeDelay` | `close-delay` | `undefined \| number` | Delay (ms) before it closes after the pointer leaves. Defaults to 300. |
| `placement` | `placement` | `undefined \| string` | Preferred placement: `'top' \| 'bottom' \| 'left' \| 'right'` (+ optional `-start`/`-end`). Defaults to `'bottom'`; flips to stay in view. |
| `open` | `open` | `undefined \| false \| true` | Drive/observe open state (Shoelace-style: settable + reflected to the `open` attribute, the element still self-manages on hover). Set `el.open = true`, or ``; listen for `kai-open-change`. |
| `defaultOpen` | `default-open` | `undefined \| false \| true` | Initial open state on mount (uncontrolled seed). |
| `disabled` | `disabled` | `undefined \| false \| true` | Suppress the hover behavior entirely without unmounting. |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-open-change` | `CustomEvent<{ open: false \| true }>` | The card opened or closed (by hover/focus, outside-click, or a method). |

**Slots** (project your own markup via `slot="name"` on a light-DOM child):

| Slot | Mode | Description |
|---|---|---|
| `card` | inject | The rich content shown in the floating hover card. |

---

### `kai-icon` / `Icon`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `name` | `name` | `undefined \| string` | A curated icon name (e.g. `"mic"`, `"globe"`), an image URL/data-URI, or plain text. |
| `size` | `size` | `undefined \| "sm" \| "md" \| "lg"` | Size token: `sm` \| `md` (default) \| `lg`. |

_No events._

**Styleable parts** (restyle from outside via `kai-icon::part(name)`):

| Part | Description |
|---|---|
| `::part(icon)` | The icon wrapper. Inherits `currentColor` and the `size` prop by default; recolor or resize it from outside. — `kai-icon::part(icon) { color: var(--color-primary) }` |

---

### `kai-image` / `Image`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `base64` | `base64` | `undefined \| string` | Base64-encoded image data (pair with `media-type`). |
| `bytes` | — | `undefined \| Uint8Array` | Raw image bytes (set as a JS property). |
| `alt` | `alt` | `undefined \| string` | Alt text. |
| `mediaType` | `media-type` | `undefined \| string` | MIME type (default `image/png`). |

_No events._

---

### `kai-input` / `Input`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `type` | `type` | `undefined \| string` | Native input type: `text` (default) · `email` · `url` · `search` · `tel` · `password` · `number`. Single-line only. |
| `value` | `value` | `undefined \| string` | Controlled value — settable and reflected to the `value` attribute. `el.value = 'hi'` drives it (no event); typing updates it and fires `kai-input`. Read `el.value` for live state. |
| `placeholder` | `placeholder` | `undefined \| string` | Placeholder shown when empty. |
| `label` | `label` | `undefined \| string` | Field label, linked to the input. |
| `hint` | `hint` | `undefined \| string` | Helper text below the control. |
| `error` | `error` | `undefined \| string` | Error text; flips the field invalid (`aria-invalid` + destructive border). |
| `size` | `size` | `undefined \| "sm" \| "md"` | Control density: `sm` or `md`. Defaults to `md`. |
| `disabled` | `disabled` | `undefined \| false \| true` | Disable interaction. |
| `readonly` | `readonly` | `undefined \| false \| true` | Make the input read-only. |
| `required` | `required` | `undefined \| false \| true` | Mark the input required. |
| `invalid` | `invalid` | `undefined \| false \| true` | Force the invalid state without an `error` string. |
| `name` | `name` | `undefined \| string` | Form-control name. |
| `autocomplete` | `autocomplete` | `undefined \| string` | Autofill hint forwarded to the inner input (e.g. `email`, `current-password`). |
| `inputmode` | `inputmode` | `undefined \| string` | Virtual-keyboard hint forwarded to the inner input (e.g. `numeric`, `email`). |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-change` | `CustomEvent<{ value: string }>` | The value was committed (blur). |
| `kai-input` | `CustomEvent<{ value: string }>` | The value changed per keystroke. |

**Slots** (project your own markup via `slot="name"` on a light-DOM child):

| Slot | Mode | Description |
|---|---|---|
| `leading` | inject | A glyph, prefix, or affix at the start of the field, inside the border. |
| `trailing` | inject | A button, unit, or affix at the end of the field, inside the border. |

**Styleable parts** (restyle from outside via `kai-input::part(name)`):

| Part | Description |
|---|---|
| `::part(field)` | The bordered control box (the row wrapping any affixes plus the input). Restyle its border, radius, surface, or focus ring. — `kai-input::part(field) { border-radius: 0.75rem }` |
| `::part(input)` | The inner input element. Restyle its text, padding, or placeholder. — `kai-input::part(input) { font-variant-numeric: tabular-nums }` |
| `::part(label)` | The field label above the control. Restyle its typography or spacing. — `kai-input::part(label) { font-weight: 600 }` |
| `::part(hint)` | The hint or error line below the control. Restyle its typography. — `kai-input::part(hint) { font-style: italic }` |

---

### `kai-kbd` / `Kbd`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `keys` | `keys` | `undefined \| string` | Shortcut spec — tokens joined by `+` (e.g. `Mod+Shift+K`). Omit it to show default-slot content instead. Display only; the element does not bind keys. |
| `platform` | `platform` | `undefined \| "other" \| "auto" \| "mac"` | `mac` uses ⌘/⌥, `other` uses Ctrl. `auto` (default) sniffs the OS. |
| `size` | `size` | `undefined \| "sm" \| "md"` | Cap size: `sm` or `md`. Defaults to `md`. |

_No events._

**Styleable parts** (restyle from outside via `kai-kbd::part(name)`):

| Part | Description |
|---|---|
| `::part(key)` | Each key cap. Restyle its surface, border, radius, or font. — `kai-kbd::part(key) { border-radius: 0.375rem }` |
| `::part(separator)` | The gap between key caps. Inject a literal joiner (e.g. a plus sign) from outside. — `kai-kbd::part(separator)::after { content: "+" }` |

---

### `kai-link-preview` / `LinkPreview`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `cardId` | `card-id` | `undefined \| string` | Stable card id correlating every emitted event. Set as an attribute or property. |
| `data` | — | `undefined \| { url: string; title?: undefined \| string; description?: undefined \| string; image?: undefined \| string; imageAlt?: undefined \| string; favicon?: undefined \| string; domain?: undefined \| string; siteName?: undefined \| string }` | The link payload (OG metadata). Set as a JS **property** (object). |

_No events._

---

### `kai-loader` / `Loader`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `variant` | `variant` | `undefined \| "circular" \| "classic" \| "pulse" \| "pulse-dot" \| "dots" \| "typing" \| "wave" \| "bars" \| "terminal" \| "text-blink" \| "text-shimmer" \| "loading-dots"` | The animation style: `'circular' \| 'classic' \| 'pulse' \| 'pulse-dot' \| 'dots' \| 'typing' \| 'wave' \| 'bars' \| 'terminal' \| 'text-blink' \| 'text-shimmer' \| 'loading-dots'`. Defaults to `'circular'`. |
| `size` | `size` | `undefined \| "sm" \| "md" \| "lg"` | Loader size: `'sm' \| 'md' \| 'lg'`. Defaults to `'md'`. |
| `text` | `text` | `undefined \| string` | Label for the text-based variants. |

_No events._

---

### `kai-markdown` / `Markdown`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `content` | `content` | `string` | The markdown source to render. |
| `proseSize` | `prose-size` | `undefined \| "sm" \| "lg" \| "xs" \| "base"` | Text/markdown sizing. |
| `codeTheme` | `code-theme` | `undefined \| string` | Shiki theme for fenced code blocks. |
| `codeHighlight` | `code-highlight` | `undefined \| false \| true` | Disable syntax highlighting (no Shiki loads). |

_No events._

---

### `kai-menu` / `Menu`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `items` | — | `undefined \| { id?: undefined \| string; label?: undefined \| string; icon?: undefined \| string; shortcut?: undefined \| string; checked?: undefined \| false \| true; radioGroup?: undefined \| string; disabled?: undefined \| false \| true; separator?: undefined \| false \| true; heading?: undefined \| false \| true; items?: undefined \| Record[] }[]` | Tree of menu items. Set as a JS property — not an HTML attribute. |
| `placement` | `placement` | `undefined \| string` | Optional placement hint (unused by the underlying Dropdown which always positions bottom-start, kept for future extension). |
| `triggerIcon` | `trigger-icon` | `undefined \| string` | Built-in trigger: leading icon (a named icon like `"plus"`, an image URL/data-URI, or text). Use this instead of slotting `slot="trigger"` for the common case — a slotted trigger overrides it. |
| `triggerLabel` | `trigger-label` | `undefined \| string` | Built-in trigger: a text label (e.g. `"High"`). |
| `triggerIconTrailing` | `trigger-icon-trailing` | `undefined \| string` | Built-in trigger: a trailing icon (e.g. `"chevron-down"` for a select look). |
| `label` | `label` | `undefined \| string` | Accessible name for an icon-only trigger (no visible label). |
| `open` | `open` | `undefined \| false \| true` | Drive/observe open state (Shoelace-style: settable + reflected to the `open` attribute, the menu still self-manages on click/keyboard). Set `el.open = true`, or ``; listen for `kai-open-change`. |
| `defaultOpen` | `default-open` | `undefined \| false \| true` | Initial open state on mount (uncontrolled seed). |
| `disabled` | `disabled` | `undefined \| false \| true` | Disable the trigger — click/keyboard and `show()` no longer open the menu. |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-open-change` | `CustomEvent<{ open: false \| true }>` | The menu opened or closed (by click, keyboard, Escape, outside-click, or a method). |
| `kai-select` | `CustomEvent<{ id: string; checked?: undefined \| false \| true; radioGroup?: undefined \| string }>` | Fired when the user selects a leaf item. - Plain items: `{ id }`. - Checkbox items: `{ id, checked }` where `checked` is the NEW state. - Radio items: `{ id, radioGroup }` — the consumer marks `id` as the selected one in `radioGroup` and clears the others. |

**Slots** (project your own markup via `slot="name"` on a light-DOM child):

| Slot | Mode | Description |
|---|---|---|
| `trigger` | replace | Your own trigger element; replaces the built-in button driven by the `trigger-icon` / `trigger-label` props. |

**Styleable parts** (restyle from outside via `kai-menu::part(name)`):

| Part | Description |
|---|---|
| `::part(shortcut)` | The right-aligned per-item keyboard shortcut, rendered as kai-kbd key caps. Shown only when an item carries a `shortcut`. — `kai-menu::part(shortcut) { opacity: 0.8 }` |

---

### `kai-message` / `Message`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `message` | — | `undefined \| { id: string; role: "user" \| "assistant"; content: string; reasoning?: undefined \| { text: string; label?: undefined \| string }; tools?: undefined \| { type: string; state: "input-streaming" \| "input-available" \| "output-available" \| "output-error"; input?: undefined \| Record; output?: undefined \| Record; toolCallId?: undefined \| string; errorText?: undefined \| string }[]; attachments?: undefined \| { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[]; actions?: undefined \| ("copy" \| "like" \| "dislike" \| "regenerate" \| "edit" \| { id: string; label: string; icon?: undefined \| string; tooltip?: undefined \| string })[]; avatar?: undefined \| { src?: undefined \| string; fallback?: undefined \| string; alt?: undefined \| string }; feedback?: undefined \| "like" \| "dislike" }` | The full message object. Set as a JS property. |
| `role` | `role` | `undefined \| "user" \| "assistant"` | Convenience for simple cases when not passing a `message` object. |
| `content` | `content` | `undefined \| string` | Convenience content (used when `message` is not set). |
| `markdown` | `markdown` | `undefined \| false \| true` | Force markdown on/off. Defaults to on for assistant, off for user. |
| `proseSize` | `prose-size` | `undefined \| "sm" \| "lg" \| "xs" \| "base"` | Text/markdown sizing for the message body. |
| `codeTheme` | `code-theme` | `undefined \| string` | Shiki theme name used for fenced code blocks in the content. |
| `codeHighlight` | `code-highlight` | `undefined \| false \| true` | Disable syntax highlighting for code blocks (no Shiki loads). |
| `actionsReveal` | `actions-reveal` | `undefined \| "always" \| "hover"` | Whether the action bar is always visible (`'always'`, default) or only revealed on hover of the message row (`'hover'`). |
| `avatarSrc` | `avatar-src` | `undefined \| string` | Convenience avatar image URL (used when `message.avatar` is not set). |
| `avatarFallback` | `avatar-fallback` | `undefined \| string` | Convenience avatar fallback text (used when `message.avatar` is not set). |
| `avatar` | `avatar` | `undefined \| string` | Avatar rail mode. `'none'` omits the avatar rail entirely so the body spans the full row (predictable layout when you never show avatars). Any other value keeps the default behaviour: the built-in avatar when one resolves, or your `slot="avatar"` content when projected (which REPLACES the built-in). |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-message-action` | `CustomEvent<{ messageId: string; action: string; state?: undefined \| "on" \| "off" }>` | An action button was clicked. `action` is the built-in name or custom id. `state` is present only for the toggleable feedback votes: `'on'` when a like/dislike is set, `'off'` when re-tapped to clear. |

**Slots** (project your own markup via `slot="name"` on a light-DOM child):

| Slot | Mode | Description |
|---|---|---|
| `before-body` | inject | A per-message header at the TOP of the body, above reasoning/tools/content — a model-name label, a role + timestamp line. |
| `after-body` | inject | A row at the BOTTOM of the body, below the action bar — a citation/sources row, a token-cost/latency line. |
| `avatar` | replace | Replaces the built-in avatar rail with your own node. Use `avatar="none"` to omit the rail and let the body span the full row. |

**Styleable parts** (restyle from outside via `kai-message::part(name)`):

| Part | Description |
|---|---|
| `::part(row)` | The message row wrapper (avatar rail + body column). Restyle its gap or alignment from outside. — `kai-message::part(row) { gap: 0.75rem }` |
| `::part(bubble)` | The content bubble wrapper. Restyle its background, radius, or padding; for a user message this is the rounded chat bubble. — `kai-message::part(bubble) { background: var(--color-primary); color: var(--color-primary-foreground) }` |
| `::part(content)` | The rendered message text/markdown region (same node as `bubble`). Target it to tune typography from outside. — `kai-message::part(content) { font-size: 0.9375rem }` |
| `::part(actions)` | The action-bar row (copy / like / regenerate …). Restyle its spacing or hide it entirely from outside. — `kai-message::part(actions) { gap: 0.25rem }` |
| `::part(avatar)` | Replaces the built-in avatar rail with your own node. Use `avatar="none"` to omit the rail and let the body span the full row. |

---

### `kai-model-switcher` / `ModelSwitcher`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `models` | — | `{ id: string; name: string; provider?: undefined \| string; description?: undefined \| string; group?: undefined \| string }[]` | The selectable models. Set as a JS property (array). |
| `currentModel` | `current-model` | `undefined \| string` | The currently-selected model id. Defaults to the first model. |
| `open` | `open` | `undefined \| false \| true` | Drive/observe the dropdown's open state (Shoelace-style: settable + reflected to the `open` attribute, the dropdown still self-manages on click/keyboard). Set `el.open = true`, or ``; listen for `kai-open-change`. |
| `defaultOpen` | `default-open` | `undefined \| false \| true` | Initial open state on mount (uncontrolled seed). |
| `disabled` | `disabled` | `undefined \| false \| true` | Disable the trigger — click/keyboard and `show()` no longer open the dropdown. |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-model-change` | `CustomEvent<{ modelId: string }>` | A model was selected. |
| `kai-open-change` | `CustomEvent<{ open: false \| true }>` | The model dropdown opened or closed (by click, keyboard, Escape, outside-click, or a method). |

---

### `kai-nav` / `Nav`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `items` | — | `undefined \| { id: string; label?: undefined \| string; icon?: undefined \| string; badge?: undefined \| string; trailing?: undefined \| string; disabled?: undefined \| false \| true; children?: undefined \| Record[]; status?: undefined \| { tone: "error" \| "primary" \| "info" \| "success" \| "warning" \| "neutral"; label?: undefined \| string; pulse?: undefined \| false \| true }; meta?: undefined \| string; action?: undefined \| { icon: string; label: string }; closable?: undefined \| false \| true }[]` | The nav items. Set as a JS property (array, not an attribute). Each item may carry `children` (a collapsible group), a `status` dot, and trailing `meta` text. |
| `value` | `value` | `undefined \| string` | Active item id (controlled). |
| `defaultValue` | `default-value` | `undefined \| string` | Initial active id when uncontrolled. |
| `defaultCollapsed` | — | `undefined \| string[]` | Ids of group items collapsed on first render (groups default to expanded). Set as a JS property (array). |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-nav-item-action` | `CustomEvent<{ value: string; action?: undefined \| { icon: string; label: string } }>` | A row's trailing `action` button was activated (not a select). `value` is the item id; `action` echoes the item's `{ icon, label }`. |
| `kai-nav-item-close` | `CustomEvent<{ value: string }>` | A `closable` row's trailing close button was activated (not a select). `value` is the item id. |
| `kai-nav-select` | `CustomEvent<{ id: string }>` | A nav item was activated. |

**Styleable parts** (restyle from outside via `kai-nav::part(name)`):

| Part | Description |
|---|---|
| `::part(nav)` | The nav list container. Restyle its gap or padding from outside. — `kai-nav::part(nav) { gap: 0.25rem }` |
| `::part(item)` | A nav item button (leaf or group parent). The active leaf carries aria-current="page" and a group parent carries aria-expanded; target `::part(item)[aria-current]` for the selected look or `::part(item)[aria-expanded]` for a group row. — `kai-nav::part(item)[aria-current] { background: var(--color-accent) }` |
| `::part(group)` | The nested child list rendered under an expanded group item. Add a left guide line or tune its indent from outside. — `kai-nav::part(group) { border-left: 1px solid var(--color-border); margin-left: 1.1rem }` |
| `::part(chevron)` | The disclosure chevron on a group row (rotates when expanded). Recolor or resize it from outside. — `kai-nav::part(chevron) { opacity: 1; color: var(--color-primary) }` |
| `::part(status)` | The per-item status cluster (a colored dot in the tone hue + an optional label). Shown only when an item carries a `status`; the `pulse` flag animates the dot. Restyle from outside. — `kai-nav::part(status) { gap: 0.5rem }` |
| `::part(meta)` | The right-aligned muted trailing text on a row (e.g. a relative time). Shown only when an item carries `meta`; restyle from outside. — `kai-nav::part(meta) { color: var(--color-foreground); font-variant-numeric: tabular-nums }` |
| `::part(item-action)` | The trailing per-item action / close button, a sibling of the item button. Shown only when an item carries `action` or `closable`; reveal it on hover or pin it visible from outside. — `kai-nav::part(item-action) { opacity: 1 }` |

---

### `kai-notice` / `Notice`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `severity` | `severity` | `undefined \| "error" \| "info" \| "success" \| "warning" \| "neutral"` | `neutral` (default) · `info` · `warning` · `error` · `success`. Drives the leading icon's color and the a11y role (`alert` for errors, else `status`). |
| `icon` | `icon` | `undefined \| string` | Leading icon: omit for the severity default, `"none"` to hide it, or a named icon to override. |
| `dismissible` | `dismissible` | `undefined \| false \| true` | Show a dismiss (×) that hides the notice and emits `kai-dismiss`. |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-dismiss` | `CustomEvent` | The notice was dismissed via its × (it also hides itself). |

**Slots** (project your own markup via `slot="name"` on a light-DOM child):

| Slot | Mode | Description |
|---|---|---|
| `action` | inject | A trailing action beside the message — a link or button. |
| `icon` | replace | A custom leading icon (any inline SVG, inherits `currentColor`). Overrides the severity default and the `icon` prop — the same escape hatch as `kai-button`. |

---

### `kai-pane` / `Pane`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `headline` | `headline` | `undefined \| string` | The pane title (the agent / window name). Named `headline` because `title` collides with the global `HTMLElement.title` attribute (it throws at registration). Attribute: `headline`. |
| `subtitle` | `subtitle` | `undefined \| string` | A role / label shown under the title (e.g. "Reviewer", "claude-sonnet"). Attribute: `subtitle`. |
| `maximized` | `maximized` | `undefined \| false \| true` | Show the restore glyph instead of maximize, and signal the maximized view-state. Drive it yourself in response to `kai-maximize`. Attribute: `maximized`. |
| `focused` | `focused` | `undefined \| false \| true` | Highlight the frame with a ring/border to mark the ACTIVE pane. Attribute: `focused`. |
| `showSplit` | `show-split` | `undefined \| false \| true` | Show a split-pane window control that fires `kai-split`. Off by default. Attribute: `show-split`. |
| `showDock` | `show-dock` | `undefined \| false \| true` | Show a dock-to-side window control that fires `kai-dock`. Off by default. Attribute: `show-dock`. |
| `status` | — | `undefined \| { tone: "working" \| "idle" \| "done" \| "error" \| "blocked"; label?: undefined \| string; pulse?: undefined \| false \| true }` | A tone-colored status dot (+ optional label) in the header. An object `{ tone, label?, pulse? }` set as a JS PROPERTY (not an attribute). |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-close` | `CustomEvent` | The close (×) control was clicked. |
| `kai-dock` | `CustomEvent` | The dock control was clicked (only present when `show-dock`). |
| `kai-maximize` | `CustomEvent<{ maximized: false \| true }>` | The maximize/restore control was clicked. `detail.maximized` is the intended NEXT state — drive the `maximized` prop yourself from it. |
| `kai-split` | `CustomEvent` | The split control was clicked (only present when `show-split`). |

**Slots** (project your own markup via `slot="name"` on a light-DOM child):

| Slot | Mode | Description |
|---|---|---|
| `leading` | inject | A glyph or avatar at the start of the pane header. |
| `actions` | inject | Extra header controls, before the built-in window controls. |
| `footer` | inject | A pinned row below the body (e.g. a composer). |

**Styleable parts** (restyle from outside via `kai-pane::part(name)`):

| Part | Description |
|---|---|
| `::part(header)` | The pane header bar (leading + title/status + actions + window controls). — `kai-pane::part(header) { padding-inline: 0.75rem }` |
| `::part(body)` | The scrolling body region (the default slot). — `kai-pane::part(body) { padding: 1rem }` |
| `::part(controls)` | The window-control cluster (maximize/close, and split/dock when enabled). — `kai-pane::part(controls) { gap: 0.25rem }` |
| `::part(footer)` | A pinned row below the body (e.g. a composer). |

---

### `kai-pane-group` / `PaneGroup`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `tabs` | — | `undefined \| { id: string; name: string; status?: undefined \| { tone: "working" \| "idle" \| "done" \| "error" \| "blocked"; label?: undefined \| string; pulse?: undefined \| false \| true }; needsAttention?: undefined \| false \| true; number?: undefined \| number }[]` | The tabs to render. An array of `{ id, name, status?, needsAttention?, number? }` set as a JS PROPERTY (not an HTML attribute). |
| `active` | `active` | `undefined \| string` | The active tab id (controlled, and reflected to the `active` ATTRIBUTE so `::part`/`[active]` selectors and the per-tab named slot follow it). Set it as the `active` attribute or drive it from `kai-tab-change`; omit for uncontrolled (the first tab). |
| `focused` | `focused` | `undefined \| false \| true` | Highlight the frame as the ACTIVE group in a multi-group layout. Attribute: `focused`. |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-tab-change` | `CustomEvent<{ id: string }>` | A tab was selected (click, Enter/Space, or arrow-key move). `detail.id` is the tab's id. |
| `kai-tab-close` | `CustomEvent<{ id: string }>` | A tab's close (×) was clicked. Drop the tab from `tabs` yourself. |
| `kai-tab-menu` | `CustomEvent<{ id: string }>` | A tab's "…" overflow was clicked. Open your own menu from `detail.id`. |

**Styleable parts** (restyle from outside via `kai-pane-group::part(name)`):

| Part | Description |
|---|---|
| `::part(tabs)` | The tab strip (role="tablist"). Restyle its background, height, padding, or gap from outside. — `kai-pane-group::part(tabs) { background: var(--color-card); gap: 0.25rem }` |
| `::part(tab)` | A single tab button. The active tab carries `[aria-selected="true"]`; target `::part(tab)[aria-selected="true"]` for the selected look. — `kai-pane-group::part(tab)[aria-selected="true"] { background: var(--color-accent) }` |
| `::part(body)` | The active tab's content region (the named/default slot host). — `kai-pane-group::part(body) { padding: 0.75rem }` |
| `::part(menu)` | The per-tab "…" overflow button. Reveal it on hover or pin it visible from outside. — `kai-pane-group::part(menu) { opacity: 1 }` |
| `::part(close)` | The per-tab close ("×") button. Recolor, resize, or hide it from outside. — `kai-pane-group::part(close) { color: var(--color-muted-foreground) }` |

---

### `kai-popover` / `Popover`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `placement` | `placement` | `undefined \| "top" \| "right" \| "bottom" \| "left" \| "top-start" \| "top-end" \| "right-start" \| "right-end" \| "bottom-start" \| "bottom-end" \| "left-start" \| "left-end"` | Floating placement relative to the trigger (floating-ui placement). |
| `gutter` | `gutter` | `undefined \| number` | Gap in px between the trigger and the panel. |
| `open` | `open` | `undefined \| false \| true` | Drive/observe open state (Shoelace-style: settable + reflected to the `open` attribute, the element still self-manages on click). Set `el.open = true`, or ``; listen for `kai-open-change`. |
| `defaultOpen` | `default-open` | `undefined \| false \| true` | Initial open state on mount (uncontrolled seed). |
| `disabled` | `disabled` | `undefined \| false \| true` | Turn the popover off while keeping the trigger mounted (clicks and `show()` no longer open it). |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-open-change` | `CustomEvent<{ open: false \| true }>` | The popover opened or closed (click, Escape, outside-click, or a method). |

---

### `kai-progress-bar` / `ProgressBar`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `value` | `value` | `undefined \| number` | Current progress value (0..max). Attribute: `value`. |
| `max` | `max` | `undefined \| number` | The value `value` runs to (default 100). Attribute: `max`. |
| `label` | `label` | `undefined \| string` | Optional caption above the track. Attribute: `label`. |
| `tone` | `tone` | `undefined \| string` | Fill color: `primary` (default), `success`, `warning`, `error`, `info`. Attribute: `tone`. |

_No events._

**Styleable parts** (restyle from outside via `kai-progress-bar::part(name)`):

| Part | Description |
|---|---|
| `::part(track)` | The progress track (the background bar). Restyle its height, radius, or background from outside. — `kai-progress-bar::part(track) { height: 0.5rem }` |
| `::part(fill)` | The filled portion; its width follows value/max. Recolor it from outside. — `kai-progress-bar::part(fill) { background: var(--color-tool-green) }` |

---

### `kai-prompt-dock` / `PromptDock`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `frame` | `frame` | `undefined \| "none" \| "inset" \| "edge"` | How the tray frames the input — the SPATIAL inset axis: `inset` (default, the classic recessed frame on every side) \| `edge` (top/bottom inset only; the input sits flush left/right so the lips span the full width) \| `none` (no inset; the lips attach directly as a plain stack). Attribute: `frame`. |
| `appearance` | `appearance` | `undefined \| "outlined" \| "filled" \| "plain" \| "soft"` | How the tray surface looks — the VISUAL axis, orthogonal to `frame`: `soft` (default, sunken surface + border + radius) \| `outlined` (transparent + border + radius) \| `filled` (sunken, no border, + radius) \| `plain` (bare). Attribute: `appearance`. |

_No events._

**Slots** (project your own markup via `slot="name"` on a light-DOM child):

| Slot | Mode | Description |
|---|---|---|
| `top` | inject | The top lip: a notice or banner above the input. Rendered only when filled. |
| `bottom` | inject | The bottom lip: a mode or controls row below the input. Rendered only when filled. |

**Styleable parts** (restyle from outside via `kai-prompt-dock::part(name)`):

| Part | Description |
|---|---|
| `::part(tray)` | The recessed tray that frames the input. The `appearance`/`frame` props set the defaults; the --kai-prompt-dock-* tokens fine-tune surface/border/radius/inset. — `kai-prompt-dock::part(tray) { --kai-prompt-dock-radius: 1rem }` |
| `::part(top)` | The top lip: a notice or banner above the input. Rendered only when filled. |
| `::part(bottom)` | The bottom lip: a mode or controls row below the input. Rendered only when filled. |

---

### `kai-prompt-input` / `PromptInput`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `value` | — | `undefined \| string \| ({ type: "text"; text: string } \| { type: "entity"; entity: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record } })[]` | Value of the input, as a JS property. A **string** is the controlled text mirror (the host owns it and updates on `kai-value-change`). A **ComposerDoc** (array of text/entity segments) is a one-time **seed** that pre-populates pills (skills/agents/plugins); the user then edits freely. Leave unset for uncontrolled behavior. `kai-submit`/`kai-value-change` always emit `value` as the flattened string (back-compat) plus the structured `doc` + `entities`. |
| `placeholder` | `placeholder` | `undefined \| string` | Placeholder text shown in the empty input. |
| `disabled` | `disabled` | `undefined \| false \| true` | Disable the input and submit button entirely (non-interactive). |
| `loading` | `loading` | `undefined \| false \| true` | Show the loading/streaming state and block submit (use while awaiting a reply). |
| `suggestions` | — | `undefined \| string[]` | Starter prompts shown above the input. Clicking one follows `suggestionMode`. Set as a JS property. |
| `suggestionMode` | `suggestion-mode` | `undefined \| "submit" \| "fill"` | What clicking a suggestion does: `'submit'` (default) sends it immediately as if typed and submitted; `'fill'` just places it in the input. |
| `search` | `search` | `undefined \| false \| true` | Show a Search (Globe) button in the left toolbar; clicking it fires a `search` event. |
| `voice` | `voice` | `undefined \| false \| true` | Show a Voice (Mic) button in the left toolbar; clicking it fires a `voice` event. |
| `stoppable` | `stoppable` | `undefined \| false \| true` | When set and `loading` is true, the send button is replaced by a Stop button (square icon, "Stop" aria-label). Clicking it fires `kai-stop`. |
| `submit` | `submit` | `undefined \| "always" \| "auto"` | Send-button visibility. `'always'` (default) always shows it; `'auto'` shows it only when there's text/attachments (an empty composer hides it — Enter still submits). To hide it entirely (Enter-only), it's pure CSS: `::part(send){display:none}` — no prop needed. Restyle via `::part(send)`. The Stop button (`stoppable` + `loading`) is unaffected. |
| `attach` | `attach` | `undefined \| false \| true` | When `false`, hides the built-in paperclip attach button even though the element otherwise supports attachments. Use this when a `+` menu in `toolbar-start` already exposes "Add files", to avoid a duplicate control. Defaults to `true`. |
| `attachments` | — | `undefined \| { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[]` | Attachments to seed the input with (so a consumer can pre-populate staged files without an upload). Set as a JS property; the element then manages its own attachment state from there (add via the paperclip, remove per chip). |
| `triggers` | — | `undefined \| { char: string; kind: string; items?: undefined \| { id: string; label: string; icon?: undefined \| string; description?: undefined \| string; group?: undefined \| string; kind?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record }[] }[]` | Rich entity triggers — each `{ char, kind, items }` opens a caret-anchored menu that inserts an atomic pill. Convention: `/` → skills, `@` → agents (plugins are the grouping/provenance of those items). Set as a JS property. |
| `kindIcons` | — | `undefined \| Record` | Default icon per entity kind (kind → image URL/data-URI) for pills/menu items without their own `icon`. Overrides the built-in agent/plugin glyphs. JS property. |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-attachments-change` | `CustomEvent<{ attachments: { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[] }>` | The staged attachments changed — a file was added (via the paperclip) or removed (per-chip ×). Carries the full current list so a consumer can react in real time (validate, show upload progress, toggle the send button). |
| `kai-search` | `CustomEvent>` | The Search (Globe) toolbar button was clicked. |
| `kai-stop` | `CustomEvent>` | The Stop button was clicked while `stoppable` and `loading` are both true. |
| `kai-submit` | `CustomEvent<{ value: string; doc: ({ type: "text"; text: string } \| { type: "entity"; entity: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record } })[]; entities: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record }[]; attachments: { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[] }>` | The user submitted the prompt (Enter or send button). `value` is the flattened text (back-compat); `doc` is the structured document and `entities` the inserted pills (skills/agents) for downstream expansion. |
| `kai-suggestion-click` | `CustomEvent<{ value: string }>` | A suggestion was clicked while `suggestion-mode="fill"`. |
| `kai-toolbar-action` | `CustomEvent<{ action: string }>` | A custom `` toolbar button was clicked. `action` is the `id` of the `` element that was clicked. |
| `kai-value-change` | `CustomEvent<{ value: string; doc: ({ type: "text"; text: string } \| { type: "entity"; entity: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record } })[]; entities: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record }[] }>` | The input changed (fires on every edit). Carries the flattened `value` plus the structured `doc` + `entities`. |
| `kai-voice` | `CustomEvent>` | The Voice (Mic) toolbar button was clicked. |

**Slots** (project your own markup via `slot="name"` on a light-DOM child):

| Slot | Mode | Description |
|---|---|---|
| `input-top` | inject | Inside the card, above the textarea (e.g. an inline status strip). For content above/below the whole card, use your own layout — that is light DOM you control. |
| `toolbar-start` | inject | Leading controls in the input toolbar — where a + menu goes. |
| `toolbar-end` | inject | Trailing controls in the toolbar, before the Send button. |

**Styleable parts** (restyle from outside via `kai-prompt-input::part(name)`):

| Part | Description |
|---|---|
| `::part(send)` | The send button. Restyle from outside, or hide it entirely (Enter-only) — hiding is pure CSS, which is why there is no `submit="never"`. — `kai-prompt-input::part(send) { display: none } /* Enter-only; or restyle: background, border-radius, … */` |

---

### `kai-reasoning` / `Reasoning`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `text` | `text` | `string` | The reasoning text to display. |
| `label` | `label` | `undefined \| string` | Trigger label. |
| `open` | `open` | `undefined \| false \| true` | Drive/observe open state (Shoelace-style: settable + reflected to the `open` attribute; the element still self-manages on trigger click + while streaming). Set `el.open = true`; listen for `kai-open-change`. |
| `defaultOpen` | `default-open` | `undefined \| false \| true` | Initial open state on mount (uncontrolled seed). |
| `streaming` | `streaming` | `undefined \| false \| true` | While true, auto-expands (and re-collapses when it flips false). |
| `markdown` | `markdown` | `undefined \| false \| true` | Render `text` as markdown. |
| `disabled` | `disabled` | `undefined \| false \| true` | Gate the disclosure trigger — programmatic `show()/hide()/toggle()` still work, but the trigger click no longer toggles. |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-open-change` | `CustomEvent<{ open: false \| true }>` | The reasoning block expanded or collapsed (via the trigger, streaming auto-open, or a method). |

---

### `kai-remote` / `Remote`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `src` | `src` | `undefined \| string` | The remote card URL. Attribute: `src`. |
| `providerOrigin` | `provider-origin` | `undefined \| string` | Exact provider origin (https: or http://localhost for dev). Attribute: `provider-origin`. |
| `envelope` | — | `undefined \| Record` | The card envelope to render. JS property only. |
| `policy` | — | `undefined \| Record` | Optional routing policy. JS property only. |

_No events._

---

### `kai-resizable` / `Resizable`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `orientation` | `orientation` | `undefined \| "vertical" \| "horizontal"` | Layout axis: `horizontal` (row, default) or `vertical` (column). |
| `maximizedIndex` | — | `undefined \| null \| number` | Which item index is maximized (null = none). Declarative source of truth. |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-change` | `CustomEvent<{ sizes: number[] }>` | Fired on drag-end / keyboard resize / visibility change. `detail.sizes` = panel sizes in percent. |
| `kai-maximize-change` | `CustomEvent<{ maximized: false \| true; index: null \| number }>` | Observe layout maximize state. |

---

### `kai-resizable-item` / `ResizableItem`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `size` | `size` | `undefined \| string` | Initial main-axis size: `"280px"` (fixed) or `"25%"`/`25` (percent). Omitted → flexible. |
| `min` | `min` | `undefined \| string` | Minimum size during resize (px or %). |
| `max` | `max` | `undefined \| string` | Maximum size during resize (px or %). |
| `locked` | `locked` | `undefined \| false \| true` | Fix this panel's size; adjacent dividers become non-draggable. |
| `hidden` | `hidden` | `undefined \| false \| true` | Hide this panel; its divider is dropped and the rest reflow. |
| `collapsed` | `collapsed` | `undefined \| false \| true` | Collapse this panel — same layout effect as `hidden` (divider dropped, the rest reflow), but it WORKS as a bare boolean from framework JSX. A plain `` in React/Solid/Vue/Svelte collapses the panel at the first render; `hidden` does not, because a JSX boolean sets neither the `hidden` attribute nor the IDL property on a custom element, so the parent never sees it. The facade reflects `collapsed` to a `collapsed` attribute the parent reads. Prefer this over `hidden` for declarative collapse. |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-change` | `CustomEvent` |  |
| `kai-maximize-change` | `CustomEvent` |  |

---

### `kai-response-stream` / `ResponseStream`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `text` | — | `undefined \| string \| AsyncIterable` | Text to stream. A string, or an `AsyncIterable` (set as a JS property — async iterables can't be HTML attributes). |
| `mode` | `mode` | `undefined \| "typewriter" \| "fade"` | Reveal animation. |
| `speed` | `speed` | `undefined \| number` | Characters/segments per tick. |
| `as` | `as` | `undefined \| string` | Element tag to render as. |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-complete` | `CustomEvent` | Streaming finished. |

---

### `kai-scope-picker` / `ScopePicker`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `availableAuthors` | — | `string[]` | Authors to offer as scope filters. Set as a JS property. |
| `availableTags` | — | `string[]` | Tags to offer as scope filters. Set as a JS property. |
| `currentLabel` | `current-label` | `undefined \| string` | The label shown on the trigger for the active scope. |
| `open` | `open` | `undefined \| false \| true` | Drive/observe the dropdown's open state (Shoelace-style: settable + reflected to the `open` attribute, the dropdown still self-manages on click/keyboard). Set `el.open = true`, or ``; listen for `kai-open-change`. |
| `defaultOpen` | `default-open` | `undefined \| false \| true` | Initial open state on mount (uncontrolled seed). |
| `disabled` | `disabled` | `undefined \| false \| true` | Disable the trigger — click/keyboard and `show()` no longer open the dropdown. |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-open-change` | `CustomEvent<{ open: false \| true }>` | The scope dropdown opened or closed (by click, keyboard, Escape, outside-click, or a method). |
| `kai-scope-change` | `CustomEvent<{ filters: undefined \| { tags?: undefined \| string[]; authors?: undefined \| string[]; contentType?: undefined \| "transcript" \| "markdown"; dateRange?: undefined \| { from: string; to: string } } }>` | A scope was chosen (`undefined` filters = "All Content"). |

---

### `kai-screen` / `Screen`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `open` | `open` | `undefined \| false \| true` | Drive/observe open state (Shoelace-style: settable + reflected to the `open` attribute; the element still self-manages). Set `el.open = true`, or ``; listen for `kai-open-change`. |
| `defaultOpen` | `default-open` | `undefined \| false \| true` | Initial open state on mount (uncontrolled seed). |
| `headline` | `headline` | `undefined \| string` | Header title text. A projected `title` slot overrides it. (Named `headline` because `title` collides with the global `HTMLElement.title` attribute.) |
| `back` | `back` | `undefined \| false \| true` | Show the back button (default true). |
| `noInert` | `no-inert` | `undefined \| false \| true` | Opt out of marking sibling elements inert/aria-hidden while open (for unusual layouts). |

**Events** (non-bubbling `CustomEvent`s — listen directly on the element):

| Event | `detail` type | Description |
|---|---|---|
| `kai-back` | `CustomEvent>` | Back navigation intent: the back button or Escape. The consumer flips their own routing in response (the screen knows nothing about the trigger). |
| `kai-open-change` | `CustomEvent<{ open: false \| true }>` | The screen opened or closed (a method, `Escape` close, or driven `open`). |

**Slots** (project your own markup via `slot="name"` on a light-DOM child):

| Slot | Mode | Description |
|---|---|---|
| `title` | replace | Rich header title; overrides the `headline` prop. |
| `actions` | inject | Header trailing cluster (e.g. an avatar or overflow menu). |

**Styleable parts** (restyle from outside via `kai-screen::part(name)`):

| Part | Description |
|---|---|
| `::part(header)` | The back-header bar (back button + title + actions). Restyle its height, padding, or border from outside. — `kai-screen::part(header) { height: 3.25rem; padding-inline: 1rem }` |
| `::part(back)` | The back button. Restyle or hide it from outside; `back="false"` removes it entirely. — `kai-screen::part(back) { border-radius: 9999px }` |
| `::part(body)` | The full-bleed surface that fills the mount point and scrolls its content. Tune padding or background from outside. — `kai-screen::part(body) { background: var(--color-card) }` |
| `::part(title)` | Rich header title; overrides the `headline` prop. |

---

### `kai-scroll-area` / `ScrollArea`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `orientation` | `orientation` | `undefined \| "vertical" \| "horizontal" \| "both"` | Which axis scrolls. `vertical` (default) · `horizontal` · `both`. The cross axis is clamped so content can't overflow it. |

_No events._

**Styleable parts** (restyle from outside via `kai-scroll-area::part(name)`):

| Part | Description |
|---|---|
| `::part(viewport)` | The scrolling container. Add padding or a max-height from outside; the thin scrollbar follows `--color-scrollbar-thumb`. — `kai-scroll-area::part(viewport) { padding-right: 0.5rem }` |

---

### `kai-scroll-button` / `ScrollButton`

**Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):

| Property | Attribute | Type | Description |
|---|---|---|---|
| `for` | `for` | `undefined \| string` | CSS id of the scroll container to control. When omitted the element walks up the DOM (outside its own shadow root) to find the nearest scrollable ancestor. Mirrors the `for` convention of `