Cheela Labs
REFERENCE

Chat packages

Three packages over one conversation store. Pick the layer that matches how much of the UI you want to own.

Three layers

PACKAGEYOU GETYOU WRITE
@cheela/web-componentA working widget from one HTML tagNothing
@cheela/uiReact components and hooksLayout, or your own markup around the hook
@cheela/clientHTTP client, conversation store, events, markdown parserAll 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

Terminal
npm install @cheela/ui

CheelaProvider

Holds the client and config. Everything else reads from its context, so several chats on a page share one authenticated client.

apiKeystringrequired

The runtime’s public key (ch_pk_…). Never the deploy key.

baseUrlstring

Override 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.

Inline arrows are safe

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

TypeScript
<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.

placeholderstring

Input placeholder.

onMessage(message: Message) => void

Fires 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) => void

Fires once per distinct error.

renderMessage(message: Message) => ReactNode

Replaces 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

TypeScript
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>
  );
}
RETURNEDTYPE
messagesreadonly Message[]
status"idle" | "submitting" | "error"
errorCheelaClientError | 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

Terminal
npm install @cheela/client

No React, no DOM assumptions. Use it for Vue, Svelte, Solid, a CLI, or a server-side integration.

TypeScript
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

apiKeystringrequired

The runtime public key.

baseUrlstring

Control plane host.

endUserTokenEndUserTokenProvider

Resolved per request. May be sync or async.

fetchImpltypeof fetch

Injectable, 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.

TypeScript
interface ConversationState {
  readonly messages: readonly Message[];
  readonly status: "idle" | "submitting" | "error";
  readonly error?: CheelaClientError;
}

Streaming events

TypeScript
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

CLASSMEANING
CheelaClientErrorBase class. Catch this to catch everything.
CheelaNetworkErrorThe request never completed. Carries the cause.
CheelaAuthError401 or 403. Wrong key, or a key not valid on this route.
CheelaApiErrorAny other non-2xx. Carries `status`.
TypeScript
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

HTML
<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

TypeScript
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

ATTRIBUTENOTES
api-keyRequired. The public key.
base-urlControl plane host.
themelight, dark, or auto
placeholderInput 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.