Chat packages
Three packages over one conversation store. Pick the layer that matches how much of the UI you want to own.
Three layers
| PACKAGE | YOU GET | YOU WRITE |
|---|---|---|
| @cheela/web-component | A working widget from one HTML tag | Nothing |
| @cheela/ui | React components and hooks | Layout, or your own markup around the hook |
| @cheela/client | HTTP client, conversation store, events, markdown parser | All of the UI |
ConversationStore is the single source of truth all three build on — the React hook bridges it through useSyncExternalStore, and the custom element wraps the same object.
@cheela/ui
npm install @cheela/uiCheelaProvider
Holds the client and config. Everything else reads from its context, so several chats on a page share one authenticated client.
apiKeystringrequiredThe runtime’s public key (ch_pk_…). Never the deploy key.
baseUrlstringOverride the control plane host. Rarely needed.
theme"light" | "dark" | "auto"default "auto"Resolved and written to a data-cheela-theme attribute you can target.
metadataRecord<string, string>Sent with every execution and recorded in traces. No credentials here.
endUserToken() => string | undefined | Promise<...>Resolves the signed-in visitor’s credential per request. Pass a function, not a string — a value read once pins whatever was true at mount.
The provider holds endUserToken in a ref behind a stable wrapper, so passing () => session?.token inline does not rebuild the client or drop the conversation on every render.
Chat
<Chat
initialMessages={saved}
placeholder="Ask me anything..."
className="my-chat"
onMessage={(message) => persist(message)}
onError={(error) => toast(error.message)}
renderMessage={(message) => <MyBubble message={message} />}
/>initialMessagesMessage[]Seeds the transcript, for restoring a saved conversation.
placeholderstringInput placeholder.
onMessage(message: Message) => voidFires once per finished assistant message. Under streaming the message is republished on every token at the same index, so this is keyed on identity and the store leaving submitting — not on the message count.
onError(error: CheelaClientError) => voidFires once per distinct error.
renderMessage(message: Message) => ReactNodeReplaces the default bubble.
Chat never takes an apiKey — it reads the shared client from context.
Primitives
Exported so you can rebuild the layout without rebuilding the behaviour: MessageList, MessageBubble, MessageActions, ChatInput, Markdown, Spinner, plus cn for class merging.
The first three take an optional onReply, which is what makes a capability’s reply buttons live — Chat wires it to sendMessage for you. Rendering one of them yourself without a handler drops reply actions rather than rendering them dead; links are unaffected. A disabled prop refuses input while a turn is in flight, matching the composer.
Hooks
import { useCheelaChat } from "@cheela/ui";
function MyChat() {
const { messages, status, error, sendMessage, reset } = useCheelaChat({
initialMessages: [],
});
return (
<form onSubmit={(e) => { e.preventDefault(); sendMessage(text); }}>
{/* your markup */}
</form>
);
}| RETURNED | TYPE |
|---|---|
| messages | readonly Message[] |
| status | "idle" | "submitting" | "error" |
| error | CheelaClientError | undefined |
| sendMessage | (text: string) => void |
| reset | () => void |
useCheelaClient() and useCheelaConfig() return the context’s client and resolved config directly. useResolvedTheme() turns auto into a concrete value.
@cheela/client
npm install @cheela/clientNo React, no DOM assumptions. Use it for Vue, Svelte, Solid, a CLI, or a server-side integration.
import { ExecutionClient, ConversationStore } from "@cheela/client";
const client = new ExecutionClient({
apiKey: "ch_pk_...",
endUserToken: () => session?.token,
});
const store = new ConversationStore(client);
const unsubscribe = store.subscribe(() => render(store.getState()));
await store.sendMessage("Where is order 8812?");ExecutionClient
apiKeystringrequiredThe runtime public key.
baseUrlstringControl plane host.
endUserTokenEndUserTokenProviderResolved per request. May be sync or async.
fetchImpltypeof fetchInjectable, for tests or a non-standard environment. Defaults to global fetch.
execute() takes the request body plus an optional signal for cancellation.
ConversationStore
Owns the transcript and the request lifecycle. It is a plain observable — subscribe, getState, getServerSnapshot — which is what lets React consume it through useSyncExternalStore without a second copy of the state.
interface ConversationState {
readonly messages: readonly Message[];
readonly status: "idle" | "submitting" | "error";
readonly error?: CheelaClientError;
}Streaming events
type ExecutionStreamEvent =
| { type: "text"; content: string }
| { type: "capability_start"; capability: string }
| { type: "capability_end"; capability: string; durationMs: number; error?: string }
| { type: "done"; result: ExecutionResult };capability_start and capability_end are what let a UI say “checking your order…” while it happens. The stream always ends with done, including on failure.
Errors
| CLASS | MEANING |
|---|---|
| CheelaClientError | Base class. Catch this to catch everything. |
| CheelaNetworkError | The request never completed. Carries the cause. |
| CheelaAuthError | 401 or 403. Wrong key, or a key not valid on this route. |
| CheelaApiError | Any other non-2xx. Carries `status`. |
import { CheelaAuthError, CheelaClientError } from "@cheela/client";
try {
await client.execute({ messages });
} catch (error) {
if (error instanceof CheelaAuthError) {
// check the key is the ch_pk_ one
} else if (error instanceof CheelaClientError) {
// network, or an API error
}
}A failed execution is not an error here — it resolves normally with status: "failed". Only transport and auth problems throw.
@cheela/web-component
Two builds, for two situations.
Script tag
<script src="https://unpkg.com/@cheela/web-component/dist/cheela-chat.js"></script>
<cheela-chat api-key="ch_pk_..." theme="auto"></cheela-chat>The loader defines the element immediately and fetches the heavier core chunk only when one mounts, resolved relative to its own script URL.
Bundler import
import "@cheela/web-component";This build bundles the core statically. Deferring it is your own bundler’s dynamic import() to make, one level up.
Attributes and API
| ATTRIBUTE | NOTES |
|---|---|
| api-key | Required. The public key. |
| base-url | Control plane host. |
| theme | light, dark, or auto |
| placeholder | Input placeholder |
All four are observed — changing one reconfigures the mounted widget rather than remounting it. configure() is the programmatic equivalent, and window.Cheela.init(element, options) mounts one imperatively.
Registration is guarded, so importing twice does not throw on a duplicate custom-element name.
Practical setup in Embed chat.