Skip to main content

Non-React Dev Docs

Written by Product Management

This article is auto-synced from its in-app version in Tai.

@nezasa/tai-chat-element is the framework-agnostic companion to @nezasa/tai-chat. It wraps each React SDK component in a native custom element, so non-React hosts — legacy Ember surfaces, marketing / partner sites, vanilla-JS pages — can embed the TAI chat experience with a single <script type="module"> and plain HTML tags. React, ReactDOM, TanStack Query, and the entire React SDK are bundled in; hosts don't need to bring React.

Related documents:

  • TAI Chat SDK — the React SDK this package wraps

  • Channels — the Streaming API under the hood

  • Auth & Security — token lifecycle and permissions required

Status: stable release line; per-commit dev snapshots publish on every push to main; @latest releases follow the same changesets workflow as the React SDK.


When to use this SDK

  • Use it if your app is not built with React (Ember, Vue, AngularJS, vanilla JS, Svelte, …).

  • Use it if you have a static HTML / marketing page that needs a TAI chat.

  • Prefer @nezasa/tai-chat if your host is already React — the native package is ~70 KB smaller and gives you typed props / refs.

  • Talk to the Streaming API directly if you need full control over rendering or you're on a non-browser runtime.


Installation

npm install @nezasa/tai-chat-element          # latest stable
npm install @nezasa/tai-chat-element@dev      # bleeding-edge snapshot

The package is published to GitHub Packages under the @nezasa scope. Same .npmrc setup as the React SDK:

@nezasa:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}

There are no peer dependencies — everything (React, ReactDOM, TanStack Query, @nezasa/tai-chat) is bundled into the single ESM file.

Release channels

Mirrors the React SDK — see the tai-chat.md § Release channels section. @latest for production, @dev for tracking main in integration environments.


Quick start

The custom elements register themselves the moment the bundle is parsed — the only host-specific question is how the bundle reaches the browser. Pick the section below that matches your host.

Bundler-based hosts (Ember, Vue, Webpack, Vite, …)

This is the typical case. After npm install (and the .npmrc setup above), import the package once from your app entry — the bundler emits the JS, the elements self-register on load, and the tag becomes usable in every template.

Ember ≥ 3.13 (auto-import in the blueprint)

ember-auto-import ships in every Ember ≥ 3.13 blueprint and handles npm imports without any ember-cli-build.js changes. From app/app.js:

import Application from "@ember/application";
import "@nezasa/tai-chat-element"; // self-registers <tai-chat>, <tai-chat-drawer>, <tai-chat-widget>
// …rest of your Application boilerplate

Then use the tag in any handlebars template:

<tai-chat
  api-base-url={{this.tai.baseUrl}}
  auth-token={{this.tai.token}}
  agent="your-agent-id"
  show-sessions
/>

Legacy Ember 2.18 / auto-import 1.x

Ember 2.18 predates ember-auto-import being part of the default blueprint, and modern ESM npm packages (like ours) don't play nicely with auto-import 1.x's CommonJS-era resolver. The reliable path is to skip auto-import altogether and copy the bundled ESM file into Ember's asset pipeline as a static <script type="module">. No source-level import needed — the elements register themselves the moment the browser parses the script.

Step 1. Install the package and broccoli-funnel (used to pull the file out of node_modules):

npm install @nezasa/tai-chat-element
npm install --save-dev broccoli-funnel

Step 2. Patch ember-cli-build.js to merge the bundled ESM file into the build's assets/ output:

// ember-cli-build.js
const EmberApp = require("ember-cli/lib/broccoli/ember-app");
const Funnel = require("broccoli-funnel");
const MergeTrees = require("broccoli-merge-trees");module.exports = function (defaults) {
  const app = new EmberApp(defaults, {
    /* …existing options… */
  });  const taiChatElement = new Funnel("node_modules/@nezasa/tai-chat-element/dist", {
    files: ["tai-chat-element.js"],
    destDir: "assets",
  });  return new MergeTrees([app.toTree(), taiChatElement]);
};

Step 3. Reference the file from app/index.html (after the existing Ember app scripts so it doesn't race the framework, though the tags would register either way — they don't depend on Ember being booted):

<!-- app/index.html -->
<script type="module" src="{{rootURL}}assets/tai-chat-element.js"></script>

Step 4. Use the tag in any .hbs template — Ember 2.18's template compiler treats <tai-chat> as a plain HTML element (the dash is what tells it this is a custom element, not a component):

<tai-chat
  api-base-url={{this.tai.baseUrl}}
  auth-token={{this.tai.token}}
  agent="your-agent-id"
  show-sessions
/>

Note on attribute bindings. Ember 2.18 passes string-literal attributes through to the DOM unchanged, which is exactly what r2wc expects. For dynamic values, the {{}}-interpolated form above stamps the resolved string into the HTML attribute. Function callbacks (onAuthError, onClose) and array-typed agent lists need to be set as JS properties on the element after it's in the DOM — see JS properties below; in Ember 2.18 a didInsertElement hook on a containing component is the usual home for that wiring.

Vue 3

Tell Vue not to treat the tag as a Vue component (otherwise it warns and strips attributes), then import the bundle from your entry:

// vite.config.ts
import vue from "@vitejs/plugin-vue";
export default {
  plugins: [
    vue({
      template: {
        compilerOptions: { isCustomElement: (tag) => tag.startsWith("tai-chat") },
      },
    }),
  ],
};

// src/main.ts
import { createApp } from "vue";
import App from "./App.vue";
import "@nezasa/tai-chat-element";
createApp(App).mount("#app");

<template>
  <tai-chat
    :api-base-url="baseUrl"
    :auth-token="token"
    agent="your-agent-id"
    show-sessions
  />
</template>

Webpack / generic bundler

Anywhere your app loads JS, add a side-effect import. The package's sideEffects field keeps the registration call from being tree-shaken:

import "@nezasa/tai-chat-element";

Raw HTML / no bundler (static page, marketing site)

If your host doesn't bundle JS — a hand-written HTML page, a static site generator that emits raw HTML — npm install the package and copy the single bundled ESM file into your public assets folder, then reference it with a <script type="module">:

npm install @nezasa/tai-chat-element
cp node_modules/@nezasa/tai-chat-element/dist/tai-chat-element.js public/assets/

<!doctype html>
<html>
  <head>
    <script type="module" src="/assets/tai-chat-element.js"></script>
  </head>
  <body>
    <tai-chat
      api-base-url="https://tai.nezasa.com"
      auth-token="<jwt>"
      agent="your-agent-id"
      show-sessions
    ></tai-chat>
  </body>
</html>

Why copy instead of <script src="/node_modules/…">? Most production servers don't expose node_modules/ to the browser. Vite dev does, but that's a development convenience — copy the file (or wire it into your static-asset build step) so dev and prod look the same.

AngularJS (legacy)

Same as raw HTML — AngularJS doesn't have a module-bundler convention, so either inline a <script type="module"> after npm install and the copy step above, or load it from your existing asset pipeline. AngularJS's $compile passes unknown tags through unchanged, so <tai-chat> renders without further configuration.


Elements

Tag

Wraps (React)

Purpose

<tai-chat>

TaiChat

Inline chat — fills its parent.

<tai-chat-drawer>

TaiChatDrawer

Slide-in side panel via React portal.

<tai-chat-widget>

TaiChatWidget

Floating FAB + popup (Intercom-style).

The underlying component behaviour is identical to the React SDK; see tai-chat.md § Components for prop semantics.


Attributes

All elements accept the shared config + display attributes below, plus element-specific ones.

Shared

Attribute

Type

Required

Purpose

api-base-url

string

yes

TAI backend URL

auth-token

string

yes

JWT for TAI's auth middleware

agent

string

no

Fixed agent id (single string). For allow-list mode (string[]), set element.agent = [...] as a JS property.

sessions-layout

string

no

How the sessions list is surfaced: "sidebar", "accordion", or "hidden". Defaults: "sidebar" for <tai-chat>, "accordion" for <tai-chat-drawer>, "hidden" for <tai-chat-widget>.

show-sessions

boolean

no

Deprecated alias — false forces sessions-layout="hidden". Prefer sessions-layout.

show-export

boolean

no

Show the export dropdown

show-thinking

boolean

no

Show thinking / tool step blocks

show-layout-switch

boolean

no

Show the layout menu — chat width plus message alignment, left-aligned by default (only appears while a conversation is active)

harness

string

no

Pre-select the execution engine for new sessions: "CLI", "THUNDER_AI", or "SUPER_BRAIN". Must be present in the agent's available_harnesses; if absent or unsupported the environment default is used.

show-harness-switch

boolean

no

Show the harness selector dropdown in the chat header. Populated from the agent's available_harnesses list — unavailable engines are not offered.

init-message

string

no

Markdown welcome message. Overrides agent_definition.welcome_message.

session-id

string

no

Resume an existing chat session by id. When set, the SDK skips its picker / auto-create paths and loads this session's history. Pair with the onSessionChange JS property to capture SDK-driven transitions. See Session continuity.

default-session-id

string

no

Seed the initial session once at mount, then let the SDK own the id (uncontrolled). Only honoured when session-id is absent — the controlled attribute wins. Use for a one-shot resume where the element should then own session switching. See Session continuity.

class-name

string

no

Extra CSS class on the React root

context is a JS-property-only prop (the value is a structured object with arbitrary nested data). See Page context.

Boolean attributes follow the standard HTML convention — presence is truthy (show-sessions or show-sessions="" both mean true), absence is falsy.

init-message accepts Markdown (same renderer as assistant messages — lists, inline links, **bold**, etc.). For starter-prompt chips below the greeting, see Starter prompts — they can't be expressed as an HTML attribute (arrays) and are set as a JS property.

<tai-chat-drawer> additional

Attribute

Type

Purpose

open

boolean

Controlled open/closed state

width

string

CSS width (e.g. "500px", "40vw")

show-backdrop

boolean

Show the dimming overlay

The drawer also exposes an onRequestExpand JS-property callback. When set, the drawer header renders a small "expand to full page" icon next to the close button. See Surface handoff.

<tai-chat-widget> additional

Attribute

Type

Purpose

position

string

"bottom-right" | "bottom-left" — FAB corner

label

string

Visible text label next to the FAB icon

width

string

Popup width

height

string

Popup height

The widget also exposes an onRequestExpand JS-property callback (same shape as the drawer). The inline <tai-chat> element exposes the inverse — onRequestCollapse — to surface the icon that hands a conversation back to a side surface. See Surface handoff.


JS properties (function callbacks + non-string config)

HTML attributes are strings. For function callbacks and complex objects, set properties on the element instance instead:

const el = document.querySelector("tai-chat");
el.onAuthError = () => window.location.assign("/login");
el.agent = ["agent-a", "agent-b"]; // allow-list mode (array)

r2wc re-renders the inner React tree whenever a property changes, so this is reactive — set the property whenever your host's state changes.

The drawer's onClose follows the same pattern:

document.querySelector("tai-chat-drawer").onClose = () => drawerClosed();


Session continuity

Every element accepts a session-id HTML attribute (and matching .sessionId JS property) plus an onSessionChange JS property. Together they let a host move a running conversation between surfaces — typically between a drawer embedded in your app shell and a full-page surface on its own route — without re-creating the session, refetching messages, or losing the agent's working context.

The host owns the value; the SDK never decides where the conversation lives.

// Drawer mounted in your Ember/Vue app shell, plus a full-page route at
// /copilot. Both elements read the same `sessionId` from a shared service.
const drawer = document.querySelector("tai-chat-drawer");
const fullPage = document.querySelector("tai-chat#full-page");// Capture SDK-driven transitions (auto-create / picker / delete) so the host
// can persist them in its own store and pass them to the other surface.
const handleSessionChange = (sessionId) => sessionStore.set(sessionId);
drawer.onSessionChange = handleSessionChange;
fullPage.onSessionChange = handleSessionChange;// Resume a session in either surface by setting the attribute / property.
drawer.setAttribute("session-id", sessionStore.get());
// Or programmatically — both are equivalent:
fullPage.sessionId = sessionStore.get();

The callback fires when the SDK creates a session (auto-create), when the user picks one from the sidebar, or when the active session is deleted (with null). It does not fire when the host changes session-id from outside — that's the host's own write, not an SDK transition.

If session-id references a session whose agent doesn't match the agent attribute, the SDK surfaces a visible error rather than silently swapping agents. Omit agent if you want the SDK to derive it from the session.

Seed once vs. control

session-id is a controlled attribute: the host owns the active id for the element's whole lifetime, so in-SDK switching (accordion select, New Chat, deletion) only takes effect if the host writes the new id back via onSessionChange. When you instead want to resume one conversation at mount and then let the element own switching on its own, use default-session-id. It mirrors React's value / defaultValue: the seed is read once at mount, is ignored while session-id is set (the controlled attribute wins), and never echoes through onSessionChange (the seed is the initial value, not a transition). A drawer that resumes a collapse-back handoff and then runs independently is the canonical case.

In free-agent mode (no agent attribute) default-session-id loads the session's history but the agent header / branding stays blank — the SDK only derives the agent from a controlled session-id. Pass agent (fixed-agent mode) when you also need the agent strip and branding to resolve.


Surface handoff

The SDK draws the "switch surface" icon directly inside the chat chrome. Hosts opt in by setting a callback; the SDK renders the button. The host owns where to navigate — the SDK does not unmount itself, close the drawer, or touch routing.

Element

Property

Icon location

Meaning

<tai-chat-drawer>

onRequestExpand

Drawer header, left of ✕

Send the conversation to a full-page surface

<tai-chat-widget>

onRequestExpand

Widget popup header, left of ✕

Send the conversation to a drawer or full page

<tai-chat>

onRequestCollapse

Inline header, right group

Send the conversation back to a side surface

// Pair with `session-id` so the target surface resumes the same chat.
const drawer = document.querySelector("tai-chat-drawer");
const fullPage = document.querySelector("tai-chat#full-page");drawer.onRequestExpand = () => {
  drawer.removeAttribute("open"); // host's choice — drawer doesn't auto-close
  router.navigate("/copilot");
};fullPage.onRequestCollapse = () => {
  router.navigate("/back");
  drawer.setAttribute("open", "");
};

onRequestCollapse is intentionally only available on <tai-chat> (the inline / full-page surface). It makes no sense from a surface that is already a side panel.

The icon is visible only when the callback is set — non-handoff hosts see no chrome change.


Page context

Tell the agent what the user is currently looking at. Pass a ChatContext envelope via the context JS property; the SDK forwards it to the backend on session creation, and the agent receives it as a ## Page context block in its system prompt for the new conversation.

The canonical shape is a two-field envelope:

import type { ChatContext } from "@nezasa/tai-chat-element";interface ChatContext {
  /** Free-form host-defined payload. Any JSON-serialisable value. */
  data?: unknown;
  /** Plain-language description of what `data` represents. */
  description?: string;
}

const drawer = document.querySelector("tai-chat-drawer");
drawer.context = {
  // Plain-language description — the "docs" for the agent. Tells it what
  // `data` represents without your host having to teach it every payload
  // schema.
  description: "User is viewing itinerary IT-12345 for the Smith family.",
  // Free-form payload. Anything JSON-serialisable: a URL string, a
  // structured object, an array. Pick whatever shape helps the agent.
  data: {
    itineraryRefId: "IT-12345",
    url: location.href,
  },
};

Why an envelope? description is a stable slot for a one-line prose framing of data — exactly the kind of input an LLM benefits from, and only the host is in a position to write it well. Keeping host keys inside data also leaves the top level free for future SDK-reserved fields without collision risk, and the unknown typing on data honestly says "host owns this." Earlier SDK versions accepted a flat object as context; the envelope is now the canonical shape — flat-object usage is deprecated.

context is a JS-property-only prop because the value is an arbitrary JSON object — nested objects and unicode payloads don't round-trip through HTML attribute serialisation reliably.

Type safety. @nezasa/tai-chat-element augments HTMLElementTagNameMap so document.querySelector("tai-chat-drawer") returns a typed element with context: ChatContext (and the rest of the element's props). TS callers get autocomplete on data / description and a compile error if they try to write a flat-shaped object to element.context.

Locked at session creation. Changing the property on a live session (e.g. when the user navigates to a different surface while the drawer is open) does NOT mutate the running conversation — the agent's system prompt is set once at the start. To apply new context, start a new session (clear session-id, or POST a new one and pass its id).


Starter prompts

Every element also accepts an init message (Markdown greeting) and a list of suggested starter prompts rendered as clickable chips below the greeting. Both override the values on the selected agent's definition (welcome_message / init_suggestions); leave them unset to use the agent defaults.

Attribute / Property

Type

Purpose

init-message / .initMessage

string

Markdown-rendered greeting shown on a fresh chat

.initSuggestions

string[]

Up to 10 starter prompts; clicking one sends it as the first user message

init-message works as an HTML attribute (plain Markdown string); initSuggestions is a JS property because arrays can't survive HTML attribute serialisation. Example:

<tai-chat
  api-base-url="https://tai.nezasa.com"
  auth-token="<jwt>"
  agent="your-agent-id"
  init-message="**Welcome back!** I can help with:&#10;- Booking changes&#10;- Trip summaries"
></tai-chat><script type="module">
  document.querySelector("tai-chat").initSuggestions = [
    "Summarise my trip",
    "What alerts are active?",
    "Draft a note for my client",
  ];
</script>

Clicking a chip sends its text as the first user message via the same handler as Enter-to-send — telemetry, session creation, and streaming behaviour stay identical.


Harness selection

The execution engine (harness) is chosen per chat session. Use show-harness-switch to let the user pick from the agent's supported engines; use harness to pre-select one programmatically. Only engines listed in the agent's available_harnesses are offered — unavailable engines are never shown.

<!-- Show the harness selector dropdown in the chat header: -->
<tai-chat
  api-base-url="https://tai.nezasa.com"
  auth-token="<jwt>"
  agent="nezasa/tai-assistant"
  show-harness-switch
></tai-chat><!-- Pre-select a specific harness without showing the dropdown: -->
<tai-chat
  api-base-url="https://tai.nezasa.com"
  auth-token="<jwt>"
  agent="nezasa/tai-assistant"
  harness="THUNDER_AI"
></tai-chat>

The chosen harness is locked for the session's lifetime — switching harness after a session starts has no effect. To use a different harness, start a new session.


Localization

Two paths, in order of decreasing magic:

Declarative — language="…" attribute

Pick one of the 13 built-in packs shipped from @nezasa/tai-chat/locales (en, de, fr, pt, it, es, nl, fi, sv, no, da, pl, cs) without any <script> block. Unknown codes log a console warning and fall back to English.

<tai-chat
  api-base-url="https://tai.nezasa.com"
  auth-token="<jwt>"
  agent="your-agent-id"
  language="de"
  show-sessions
></tai-chat>

Programmatic — strings property

Pass a built-in pack or a Partial<TaiChatStrings> to override individual keys. Programmatic-only because some values are functions (counts, dates, dynamic text) that can't be expressed as kebab-case strings; missing keys fall back to English. Use this path when you need per-key tweaks on top of a built-in pack, or when you ship custom translations.

<tai-chat
  api-base-url="https://tai.nezasa.com"
  auth-token="<jwt>"
  agent="your-agent-id"
  show-sessions
></tai-chat><script type="module">
  import { de } from "@nezasa/tai-chat/locales";
  document.querySelector("tai-chat").strings = de;
</script>

When both language and strings are set, strings wins per-key. That's the right shape for "start from the German pack, but rename the drawer header": <tai-chat language="de"> + el.strings = { drawerHeader: "My App Chat" }.

r2wc re-renders the inner React tree whenever the property or attribute changes, so swapping locales at runtime ("user changed language in the host app") is as simple as reassigning el.strings or el.setAttribute("language", "fr").

Switching language at runtime

Hosts that already track the user's language (URL locale, profile setting, i18n library) can mirror it into el.strings whenever it changes — the chat re-renders in the new language with no element remount or session loss. Pre-import the packs you support, build a lookup, and reassign on change:

<tai-chat
  api-base-url="https://tai.nezasa.com"
  auth-token="<jwt>"
  agent="your-agent-id"
  show-sessions
></tai-chat><select id="lang-picker">
  <option value="en">English</option>
  <option value="de">Deutsch</option>
  <option value="fr">Français</option>
</select><script type="module">
  import { de, en, fr } from "@nezasa/tai-chat/locales";  const PACKS = { en, de, fr };
  const el = document.querySelector("tai-chat");
  const picker = document.querySelector("#lang-picker");  el.strings = PACKS.en;
  picker.addEventListener("change", (e) => {
    el.strings = PACKS[e.target.value] ?? PACKS.en;
  });
</script>

Same shape from any host with a language event — e.g. an Ember service:

this.languageService.on("change", (lang) => {
  document.querySelector("tai-chat").strings = PACKS[lang] ?? PACKS.en;
});

Date strings caveat. sessionTimeToday / sessionTimeWeek / sessionTimeOlder use date-fns's English weekday/month names across every shipped pack (intentional, see tai-chat.md → Switching language at runtime). Override those three function-valued keys with a custom formatter if you need locale-aware date text.

Partial overrides

To retune one or two keys without forking a whole pack — assign an object literal that spreads the base pack:

import { de } from "@nezasa/tai-chat/locales";
document.querySelector("tai-chat").strings = {
  ...de,
  sessionsHeader: "Meine Chats",
};

The same strings property is supported on <tai-chat-drawer> and <tai-chat-widget>. For the full list of keys, function signatures, the canonical English pack, and partial-override patterns, see the @nezasa/tai-chat Localization docs.


CSS & theming

Shadow DOM (open)

Each element renders inside its own open shadow root. Host stylesheets cannot select elements inside the chat tree — so resets like aside { display: block }, button { margin: 0 }, or * { box-sizing: ... } shipped by Bootstrap and similar legacy stacks no longer collide with the SDK's Tailwind utilities. This is the structural fix for the cascade collision originally surfaced in TAI-192.

mode: "open" means devtools and end-to-end tests can still reach into the tree via element.shadowRoot.querySelector(...) — the boundary is for CSS, not for hiding implementation.

Stylesheet adopted into the shadow root

The SDK's compiled CSS is parsed once into a shared CSSStyleSheet and adopted via adoptedStyleSheets on every shadow root the package creates. The browser caches the parsed rule set across instances, so a page with many <tai-chat-*> elements doesn't re-pay the parsing cost. Nothing is appended to document.head.

Theme tokens still cross the boundary

CSS custom properties (--primary, --background, --border, …) inherit through shadow roots by default, so the theme prop keeps configuring SDK colours and font from the host side. If your host sets these tokens at a scope that covers the element (e.g. :root), the SDK picks them up — set theme explicitly to override. Conflicts where the host and the SDK define the same tokens at the same scope no longer require shadow-piercing selectors; pass theme and you're done.


Interop notes

Multiple elements on one page

Each element instantiates its own TaiChatProvider with its own QueryClient. This matches the React SDK's per-instance model — sessions and cached data aren't shared across two <tai-chat> tags on the same page.

Browser support

Custom elements are baseline in every evergreen browser (Chrome, Edge, Firefox, Safari). IE11 is not supported.

SSR

Out of scope. Custom elements are a client-only feature. If you SSR your host (Next.js, Remix, Nuxt), render the element after hydration.

Content Security Policy

The SDK CSS lives on adoptedStyleSheets rather than <style> elements, so hosts on a strict style-src directive (no 'unsafe-inline') work without further configuration: constructable stylesheets aren't governed by style-src because no stylesheet element is created. No 'unsafe-eval' is required either.

The class-name vs class attribute

r2wc maps camelCase React props to kebab-case HTML attributes, so the className prop on the underlying React component is exposed as class-name on the element. The native class attribute is independent — it styles the custom element's own box (outside the React tree) because that's how browsers handle it on any DOM element. If you want to pass a class into the React root, use class-name; if you just want to style the element's layout position, use class.

Bundle size

The element bundle is meaningfully larger than the React SDK because React, ReactDOM, and TanStack Query ship inside (the React SDK treats them as peer deps). A gzipped ceiling is enforced by scripts/check-bundle-size.mjs and CI fails over it — see the rendered dist/bundle-analysis.html (uploaded as a CI artifact on every PR) for the per-dep breakdown. Lazy-loaded deps in the SDK (mermaid, highlight.js, react-table) stay lazy here too and don't count toward the initial download. If the size becomes a concern for a particular host, lazy-load the module (await import("@nezasa/tai-chat-element")) so it lands after the host's critical path.


Release flow

See .changeset/README.md for the bump-level policy, changeset workflow, and release mechanics. The element package publishes alongside the React SDK under the same pipeline.

Changes to the element's public surface (tag names, attributes, default behaviour) are picked up by scripts/check-api-surface.mjs on every PR. If the change is intentional, regenerate the snapshot via npm run check:api -- --write --workspace=@nezasa/tai-chat-element and commit the updated api-snapshot.txt.

Did this answer your question?