This article is auto-synced from its in-app version in Tai.
@nezasa/tai-chat is a reusable, embeddable React chat component backed by TAI's Streaming API. It is one of TAI's Channels — delivering the same chat experience as the Admin Console's TAI Terminal (SSE streaming, session management, thinking / tool-step display, markdown and mermaid / draw.io rendering, file upload, export) to any React application.
If you want to add a TAI-powered chat to an internal tool, a customer-facing app, or an embedded assistant widget, this SDK is the fastest path.
Related documents:
Channels — the Streaming API this SDK talks to
Agents — how to provision agents you'll reference from
agent=Auth & Security — token lifecycle and permissions required
Status: stable release line. Per-commit dev snapshots ship on every push to main; consumers can install @nezasa/tai-chat@latest (stable) or @nezasa/tai-chat@dev (bleeding edge) — see Release channels below. The API surface is stable within a major version and follows semver.
When to use this SDK
Use it if your app is built with React 18 or 19.
Use it if you want SSE streaming, session lifecycle, and rich rendering without re-implementing them.
Use
@nezasa/tai-chat-element— the Web Component wrapper — for non-React hosts like Ember, Vue, AngularJS, or plain HTML.Talk to the Streaming API directly (Channels) if you need full control over rendering or you're on a non-browser runtime.
Installation
npm install @nezasa/tai-chat
The SDK is published to GitHub Packages under the @nezasa scope. Make sure your .npmrc authenticates to npm.pkg.github.com:
@nezasa:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}
First-time setup requires more than the snippet above — see Consumer setup — GitHub PAT + SSO authorization for the full walkthrough.
Peer dependencies
Package | Version |
|
|
|
|
|
|
React Query is a peer dependency on purpose: most host apps already use it, and sharing a single QueryClient avoids duplicate caches (see TaiChatProvider props).
Axios, marked, DOMPurify, highlight.js, mermaid, and Lucide icons are bundled as regular dependencies — you don't install them yourself.
Bundle size & lazy loading
Chunk | Uncompressed | Gzipped | When it loads |
Main entry ( | ~485 KB | ~115 KB | On first render |
| ~1.31 MB | ~355 KB | First fenced code block |
| ~124 KB | ~26 KB | First markdown table |
| ~53 KB | ~14 KB | First mermaid block |
mermaid, highlight.js, and @tanstack/react-table are lazy-loaded on first use — plain-text conversations don't pay for them. Mermaid's per- diagram-type grammars (flowchart, sequence, gantt, …) are additionally split into their own chunks and only fetched when a diagram of that type appears.
CI enforces a 150 KB gzipped budget on the main entry (make sdk-bundle-check) and grep-asserts the three heavy deps are not reachable from the main bundle, so widget-mode consumers won't silently regress into a multi-megabyte first load.
Consumer setup — GitHub PAT + SSO authorization
Before npm install @nezasa/tai-chat will succeed, two things must be in place: the .npmrc above, pointing the @nezasa scope at GitHub Packages, and a GitHub Personal Access Token that is (a) scoped to read packages and (b) SSO-authorized for the nezasa org. The token alone works on personal projects but 401s on org-scoped packages until it's SSO-authorized.
If you haven't already, drop this into your project's .npmrc (or ~/.npmrc) so the registry and auth reference are in one place:
@nezasa:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}
Then follow the four steps below to create and authorize the NPM_TOKEN.
1. Create a classic Personal Access Token
GitHub Packages for @nezasa requires a classic PAT. The newer fine-grained tokens don't support the GitHub Packages API yet.
Go to https://github.com/settings/tokens — click Generate new token → Generate new token (classic).
Note: something like
nezasa-packagesso you can find it later.Expiration: 90 days is a reasonable default. Shorter is safer but means more rotation.
Scopes: check
read:packagesonly. That's the one scope needed to install from GitHub Packages. Do not checkwrite:packages,delete:packages, orrepo— consumer workflows don't publish, and a broader token is a bigger blast radius if it leaks.Click Generate token. Copy the
ghp_…value once — GitHub won't show it again.
2. SSO-authorize the token for nezasa
The token is now created but unusable against @nezasa packages. You need to authorize it for the org's SAML SSO:
Stay on the token's page (or reopen it from https://github.com/settings/tokens). Below the scopes you'll see a Configure SSO dropdown.
Click Configure SSO → Authorize next to
nezasa. GitHub redirects you through the Nezasa SSO login.Back on the token page, the row for
nezasashould now show Authorized.
Without this step, npm install fails with a 401 Unauthorized that looks like the package doesn't exist — the error message doesn't mention SSO. See GitHub's SAML SSO docs for the org-admin perspective.
3. Store the token securely
Recommended: export NPM_TOKEN=ghp_… in your shell profile (~/.zshrc, ~/.bashrc). The repo-level .npmrc references ${NPM_TOKEN}, so the raw token never touches a committed file. This is what every Nezasa repo's .npmrc already assumes.
Alternatives if you prefer: a machine keychain (security add-generic-password on macOS, pass on Linux) that exports NPM_TOKEN at shell start, or a user-level ~/.npmrc outside any repo.
Don't paste the raw ghp_… value into a committed .npmrc, CI logs, Slack, or PR descriptions; don't share a PAT between engineers (each one creates their own); don't reuse your local PAT for CI — runners use a separate service-account PAT, or secrets.GITHUB_TOKEN inside nezasa/* repos, which GitHub provides automatically.
4. Verify the setup
Before kicking off a full npm install, sanity-check the auth with a one-liner:
NPM_TOKEN=ghp_… npm view @nezasa/tai-chat@latest version
A successful run prints the latest published version (e.g. 0.1.1). 401 Unauthorized means the token is missing, expired, lacks read:packages, or isn't SSO-authorized — see step 2.
Release channels
The SDK is published to GitHub Packages on three dist-tags:
Dist-tag | What it points at | Install |
| Tagged releases ( |
|
| An immutable snapshot of every commit merged to |
|
| Per-PR snapshots — every push on a PR touching |
|
Browse all published versions, dist-tag pointers, and download counts on the package's GitHub Packages page (requires GitHub access to the nezasa org).
Use latest for production code and any deployment you don't want to re-pin every day. Pin to a caret range ("^1.0.0") and React updates safely.
Use dev for cross-repo integration work where you need bleeding-edge SDK changes before they're tagged — e.g. a tripcache-be PR that depends on a new SDK prop you just merged. Snapshot versions are immutable, so reinstalling @dev later picks up the next snapshot. Pin to a specific snapshot (e.g. @0.1.1-dev-20260415120000-abc1234) when you want a deterministic build.
Use pr-<NUMBER> to verify a PR's exact build in a downstream repo before the PR merges. The workflow's job summary (Actions → "SDK Publish" → "Publish snapshot" → bottom of the run page) shows the exact version and the install commands. The dist-tag floats — every push to that PR republishes to the same tag — so re-running npm install @pkg@pr-NNN picks up the latest. When you want a deterministic build, pin to the exact version (e.g. @0.1.2-pr.214-20260509101501-9b4082a). Snapshots are scoped to the PR's lifetime: nothing prunes the immutable versions, but the dist-tag becomes meaningless after merge — you'd install @dev (post-merge) or @latest (post-release) instead.
Note for npm v7+ hosts: snapshot versions are SemVer prereleases (X.Y.Z-pr.NNN-...), so they don't satisfy plain caret ranges like ^X.Y.Z. Install by exact version or dist-tag, and don't commit the resulting package.json / lockfile change to your downstream repo unless you intend to ship that snapshot.
Every snapshot — dev and pr-* — runs through the same build/verify pipeline (bundle-size budget, API-surface snapshot, full test suite, pre-publish smoke test that packs the SDK, installs it into a fresh React project, and mounts each embedding mode in jsdom) before publish. A regression is caught in the PR — not after it's been published.
Versioning policy
The bump levels for any change to the SDK are documented in the repo's .changeset/README.md:
major — removing or renaming a prop, changing a prop type, removing or renaming an exported component or hook, removing a CSS custom property, narrowing the React / React-Query peer-dep range, changing default behaviour in a way consumers must adapt to.
minor — new optional prop, new exported component or hook, new CSS custom property, new optional
TaiChatConfigfield, new default behaviour that's purely additive.patch — bug fixes, internal refactors that don't change the public surface, doc-only changes inside the SDK package.
Internal contributors: every PR touching sdk-packages/** must include a changeset. CI's Changeset Gate job enforces this.
Quick start
The minimum setup is a provider + the chat component:
import { TaiChat, TaiChatProvider } from "@nezasa/tai-chat";
import "@nezasa/tai-chat/style.css";function App() {
const token = useMyAuth(); // whatever your app uses for bearer tokens return (
<TaiChatProvider
config={{
apiBaseUrl: "https://tai.nezasa.com",
authToken: token,
onAuthError: () => refreshToken(),
}}
>
<TaiChat showSessions showExport showThinking />
</TaiChatProvider>
);
}
That's it — agent selection, session sidebar, and chat rendering are all handled by the SDK.
What the consumer owns
The SDK is stateless about auth and routing. Your app is responsible for:
Acquiring and refreshing the bearer token. The SDK reads
config.authTokenon every request; pass a fresh value (or re-render the provider with a new token) whenever you rotate.Handling
onAuthError. When the backend returns 401, the SDK calls your callback. Refresh the token, redirect to login, or show an error — your call.Mounting the provider at the right level. The provider creates an internal
QueryClient. If your app already uses React Query and you want to share the cache, pass your ownqueryClientto the provider (seeTaiChatProviderprops).
Before you mount
Create an agent in TAI — see Agents. You'll use either its UUID or
agent_idin theagentprop.Ensure your token has the
agent:executepermission (see Auth).If you're embedding on a different origin than the TAI backend, make sure CORS is configured for your domain.
Embedding modes
Mode | Component | DOM strategy | Typical use case |
Inline |
| Renders in parent | Full-page chat, embedded panel |
Drawer |
| React portal + slide-in overlay | Side panel (Jira-style) |
Widget |
| React portal + floating FAB + popup | Support chat (Intercom-style) |
Web Component |
| Custom element wrapping this SDK | Non-React hosts |
Portal modes keep the chat inside the same React tree as your app, so context, theme, and the React Query cache work transparently — no iframe, no separate runtime.
Live showroom: see Inline, Drawer, and Widget rendered side-by-side at Docs → SDKs → TAI Chat SDK — Showroom. The page includes a prop-toggle panel for the Inline mode and copy-ready snippets for each mode.
Inline
<TaiChatProvider config={config}>
<div className="h-screen">
<TaiChat showSessions showExport showThinking showLayoutSwitch />
</div>
</TaiChatProvider>
<TaiChat> stretches to fill its parent's height. Give it an ancestor with an explicit height (h-screen, h-[600px], a flex parent with flex-1, etc.).
Drawer
function SupportButton() {
const [open, setOpen] = useState(false);
return (
<TaiChatProvider config={config}>
<button onClick={() => setOpen(true)}>Ask TAI</button>
<TaiChatDrawer
open={open}
onClose={() => setOpen(false)}
width="420px"
agent="support-agent"
/>
</TaiChatProvider>
);
}
Widget
<TaiChatProvider config={config}>
{/* your app */}
<TaiChatWidget
position="bottom-right"
label="Ask TAI"
agent="support-agent"
showSessions={false}
showThinking={false}
/>
</TaiChatProvider>
Props reference
<TaiChatProvider>
The provider supplies config, theme, and an internal React Query client. It must wrap every <TaiChat* /> in your tree.
Prop | Type | Default | Purpose |
|
| required | API base URL + auth |
|
|
| Optional theme overrides (see Theming) |
|
| internally created | Optional: reuse your app's React Query client |
interface TaiChatConfig {
/** Base URL of the TAI API (e.g., "https://tai.nezasa.com") */
apiBaseUrl: string;
/** Bearer token — consumer manages lifecycle */
authToken: string;
/** Called on 401 — consumer refreshes token or redirects to login */
onAuthError?: () => void;
}
<TaiChat>
Prop | Type | Default | Purpose |
|
|
| Fixed agent (string), allow-list (array), or all (undefined) |
|
| varies by surface | How the sessions list is surfaced. See "Sessions layouts" below. |
|
| — | Deprecated alias; |
|
|
| Show/hide export dropdown (Markdown / ZIP / PDF) |
|
|
| Show/hide thinking and tool-step blocks |
|
|
| Show/hide the chat-column width toggle (only appears while a conversation is active) |
|
| — | Extra class on the root container |
The agent prop has three modes:
Fixed (
agent="your-agent-id"): no selector is rendered; the SDK opens a session against that agent automatically. Pass either the UUID or the namespacedagent_id.Allow-list (
agent={["foo", "bar"]}): selector is shown, but limited to those agents.All (omit the prop): selector lists every agent the user can access.
<TaiChatDrawer>
Extends all <TaiChat> props, adds:
Prop | Type | Default | Purpose |
|
| required | Whether the drawer is visible |
|
| required | Called on backdrop click or Escape |
|
|
| Drawer width |
|
|
| Render a translucent backdrop |
<TaiChatDrawer> defaults sessionsLayout to "accordion" — a sticky "Recent sessions" bar sits between the agent strip and the chat body and expands in place (cap: 320px tall, internal scroll). Pass sessionsLayout="sidebar" for a wide drawer where the full-page panel fits, or sessionsLayout="hidden" to drop sessions UI entirely. Legacy callers using showSessions={true} keep the same intent as before (forces "sidebar"); showSessions={false} forces "hidden".
<TaiChatWidget>
Extends all <TaiChat> props, adds:
Prop | Type | Default | Purpose |
|
|
| Corner placement |
|
|
| FAB label text |
|
|
| Popup width |
|
|
| Popup height |
<TaiChatWidget> defaults sessionsLayout to "hidden" — the popup is narrow and meant to feel lightweight. Wider widgets can opt in with sessionsLayout="sidebar" (full-height panel) or sessionsLayout="accordion" (collapsible bar). Legacy showSessions={true} still maps to "sidebar" for back-compat.
Sessions layouts
Three layouts trade real-estate for discoverability:
"sidebar"(default for inline<TaiChat>): full-height left-hand panel, always visible. Best when the chat column has ≥600px of horizontal room."accordion"(default for<TaiChatDrawer>): collapsible "Recent sessions" bar between the agent strip and chat body. Always discoverable, costs ~36px when collapsed; expanded panel caps at 320px and scrolls internally so the composer stays visible."hidden"(default for<TaiChatWidget>): no sessions UI rendered, and the sessions API call is skipped entirely.
Theming
The SDK's CSS is scoped under the [data-tai-chat] attribute that the root component writes on its own container. This prevents style leakage in either direction: host styles don't bleed into the chat, chat styles don't touch the host.
theme prop (recommended)
<TaiChatProvider
config={config}
theme={{
primaryColor: "221 83% 53%", // HSL triplet — no `hsl()` wrapper, no hex
borderRadius: "0.75rem",
}}
>
<TaiChat />
</TaiChatProvider>
Important: color values must be space-separated HSL triplets ("221 83% 53%"), not hex ("#2563EB") or rgb(). The SDK composes colors with opacity via hsl(var(--primary) / 0.4), which requires the raw triplet form. Hex and named colors will silently produce broken theming.
Theme key | CSS variable | Value format | Default |
|
| HSL triplet | Nezasa red |
|
| HSL triplet | white / dark slate |
|
| HSL triplet | near-black / near-white |
|
| HSL triplet | light gray / dark slate |
|
| HSL triplet | light border / dark border |
|
| CSS length |
|
|
| CSS font stack | Mulish / system stack |
| — |
|
|
Raw CSS overrides
If you'd rather not use the prop, target the variables directly on the [data-tai-chat] container the SDK writes:
[data-tai-chat] {
--primary: 221 83% 53%;
--radius: 0.75rem;
}
Dark mode
Picked up automatically via prefers-color-scheme: dark. To force a mode (e.g. when your app has its own theme toggle), pass theme={{ colorScheme: "dark" }} — the provider writes data-tai-chat-theme="dark" on the root, which overrides the OS preference. This matters for Mermaid diagrams, whose palette is baked into the rendered SVG rather than picked up from CSS.
Hooks (advanced)
If you need to render custom UI alongside chat (a dashboard tile, a "recent conversations" list, etc.), the SDK exports its React Query hooks:
import { useRecentSessions } from "@nezasa/tai-chat";function RecentSessionsTile() {
const { data, isLoading } = useRecentSessions();
if (isLoading) return <Spinner />;
return <ul>{data?.data.map(renderSession)}</ul>;
}
Exported hooks:
useRecentSessions()useChatSessions(agentId)useChatSession(sessionId)useChatMessages(sessionId)useCreateSession()useDeleteSession()useStarSession()useUnstarSession()
All hooks use the provider's QueryClient. If you passed your own via TaiChatProvider.queryClient, the SDK's cache lives alongside yours.
Localization
The SDK ships with English by default plus 6 starter packs (de, fr, it, nl, pt, es). Pass any pack — or your own partial override — to TaiChatProvider via the strings prop. Missing keys always fall back to English, so a partial override is a safe drop-in.
Available packs
Code | Language | Status |
| English | Canonical (always up to date) |
| German (Deutsch) | Starter — full key coverage, audited TAI-162 |
| French (Français) | Starter — full key coverage, audited TAI-162 |
| Italian (Italiano) | Starter — full key coverage, audited TAI-162 |
| Dutch (Nederlands) | Starter — full key coverage, audited TAI-162 |
| Portuguese (Português) | Starter — full key coverage, audited TAI-162 |
| Spanish (Español) | Starter — full key coverage, audited TAI-162 |
Non-English packs are best-effort starter translations — useful out of the box, but expect to override individual keys to match your tone or region (see "Partial override" below). When the canonical English pack gains a key, the same key in non-English packs would temporarily fall back to English until a translator passes through; the locales.test.ts guard rejects merges that introduce that fallback for new static-string keys, so each new key must ship with a translation in every pack.
Use a built-in pack
import { TaiChat, TaiChatProvider } from "@nezasa/tai-chat";
import { de } from "@nezasa/tai-chat/locales";<TaiChatProvider config={{ apiBaseUrl, authToken }} strings={de}>
<TaiChat />
</TaiChatProvider>
Partial override
strings accepts Partial<TaiChatStrings>. Override only the keys you want; everything else falls back to English (or to the base pack you spread first).
Hoist the override to a module constant. The provider memoises its context value on the strings reference; an inline literal (one that's re-allocated on every parent render) busts the memo and rebuilds the context — and therefore re-renders every consumer of useTaiChatStrings — on every keystroke:
import { TaiChat, TaiChatProvider } from "@nezasa/tai-chat";
import { de } from "@nezasa/tai-chat/locales";// Module-level — stable identity across renders.
const STRINGS = {
...de,
sessionsHeader: "Meine Chats",
showMore: (n: number) => `${n} weitere anzeigen`,
};export function App() {
return (
<TaiChatProvider config={{ apiBaseUrl, authToken }} strings={STRINGS}>
<TaiChat />
</TaiChatProvider>
);
}
If the override depends on host state, wrap it in useMemo keyed on that state — same effect, kept in scope:
const strings = useMemo(
() => ({ ...de, sessionsHeader: customHeader }),
[customHeader],
);
Build a custom pack from scratch
If your language isn't shipped, type your pack against the TaiChatStrings interface — TypeScript guarantees you cover every key:
import type { TaiChatStrings } from "@nezasa/tai-chat";export const ja: TaiChatStrings = {
sessionsHeader: "セッション",
newChatButton: "新しいチャット",
// ... cover every key (see the canonical `en` pack for the full list)
showMore: (n) => `さらに${n}件を表示`,
messageCount: (n) => `${n}件のメッセージ`,
sessionTimeToday: (date) => `今日 ${date.toTimeString().slice(0, 5)}`,
// ...
};
Or, if you'd rather not enumerate ~80 keys, type the pack as Partial<TaiChatStrings> and let the runtime fall back to English for the keys you skip — handy when bootstrapping a new locale incrementally.
Switching language at runtime
Most multilingual hosts already track the user's language somewhere (URL locale, profile setting, i18next, react-intl, …). Mirror that into the strings prop and the chat re-renders in the new language with no remount or session loss. Three things to know:
Look the pack up by code, don't inline the spread. The provider memoises its context value on the
stringsreference. Module-level pack imports (en,de,fr) are stable; an inline literal ({ ...de, foo: "bar" }) gets a fresh identity on every parent render and rebuilds the context.No remount needed. The active session, scroll position, and draft message survive the swap — the SDK only re-renders text.
Date formats.
sessionTimeToday/sessionTimeWeek/sessionTimeOlderkeep using date-fns's English weekday/month names across every shipped pack (intentional, see the docstring onlocales/en.ts). If you need locale-aware date strings, override those three function-valued keys with your own formatter.
import { useState } from "react";
import { TaiChat, TaiChatProvider } from "@nezasa/tai-chat";
import { de, en, fr } from "@nezasa/tai-chat/locales";// Hoisted, module-level — keeps each pack reference stable across renders.
const PACKS = { en, de, fr } as const;
type Lang = keyof typeof PACKS;export function ChatWithLanguagePicker({ apiBaseUrl, authToken }: Props) {
const [lang, setLang] = useState<Lang>("en"); return (
<>
<select value={lang} onChange={(e) => setLang(e.target.value as Lang)}>
<option value="en">English</option>
<option value="de">Deutsch</option>
<option value="fr">Français</option>
</select> <TaiChatProvider config={{ apiBaseUrl, authToken }} strings={PACKS[lang]}>
<TaiChat />
</TaiChatProvider>
</>
);
}
Already wired into a host i18n library? Drive the lookup off whatever it exposes — i18next.language, useIntl().locale, next-intl's useLocale(), etc. — and pass PACKS[hostLocale] (with an en fallback for codes you don't ship):
const lang = (PACKS as Record<string, TaiChatStrings>)[hostLocale] ?? en;
<TaiChatProvider config={{ apiBaseUrl, authToken }} strings={lang}>
<TaiChat />
</TaiChatProvider>;
Function values
Keys with interpolated values (counts, dates, dynamic text) are functions so consumers control plural rules and word order without a runtime template parser:
showMore: (count: number) => string; messageCount: (count: number) => string; sessionTimeToday: (date: Date) => string; sessionTimeWeek: (date: Date) => string; sessionTimeOlder: (date: Date) => string; agentSkills: (count: number) => string; agentConnectors: (count: number) => string; agentSources: (count: number) => string; agentNotFound: (agentRef: string) => string; partialMessageBadge: (terminationReason: string) => string; imageLoadFailed: (filename: string) => string;
See the TaiChatStrings interface in source for the full list (~80 keys, grouped by UI area).
What's NOT in strings
Per-agent content — the welcome_message and init_suggestions defined on each agent — is not part of TaiChatStrings. It's localized server-side: the backend reads the request's Accept-Language header (forwarded automatically by browsers), picks the best matching locale from the agent's per-locale map (welcome_message_locales, init_suggestions_locales), and serves the flat welcome_message / init_suggestions fields the SDK already consumes. No SDK prop change is required.
Hosts that need to force a specific locale (e.g. for integration tests, or when the host runs in a non-browser environment that does not send Accept-Language) can append ?locale=de to agent API calls; the backend falls back to en for unsupported codes. The supported locale set mirrors the SDK locale packs above (en, de, es, fr, it, nl, pt).
See docs/agents.md for the full schema and locale-resolution priority.
Versioning & backward compatibility
The SDK follows semantic versioning. Breaking changes (prop removal/rename, type changes, exported-component rename, CSS custom property removal) always mean a major version bump.
Pin to a caret range (
"@nezasa/tai-chat": "^1.0.0") to receive patch and minor updates safely.Read the changelog before upgrading a major version.
Do not rely on internal exports or the shape of
dist/; only the documented components, props, hooks, and CSS variables are part of the public API.
Troubleshooting
npm install fails with 401 Unauthorized. Most common cause: the PAT isn't SSO-authorized for the nezasa org. The token alone lets you install from personal repos but 401s on org-scoped packages. Open the token at https://github.com/settings/tokens, click Configure SSO → Authorize next to nezasa, and re-run. See Consumer setup — GitHub PAT + SSO authorization for the full walkthrough. Other causes: PAT missing read:packages scope, PAT expired, $NPM_TOKEN not set in the shell running npm install.
The chat renders but is blank / unstyled. You likely forgot to import the stylesheet. Either add import "@nezasa/tai-chat/style.css" once in your app, or confirm your bundler honors the "sideEffects" entry in the SDK's package.json (default Vite and webpack setups do).
Classes like max-w-3xl do nothing. If your app uses Tailwind 4, its auto-content detector does not scan node_modules by default. Add an @source directive to your Tailwind entry pointing at the SDK:
@import "tailwindcss"; @source "../../node_modules/@nezasa/tai-chat/dist/**/*.js";
401 loops. onAuthError is expected to refresh the token and let the SDK retry on the next user action — don't re-render the provider in a way that remounts <TaiChat> on every error.
Session doesn't persist across reloads. Sessions are server-side objects; the SDK picks the most recent one on mount. If you want a specific session re-opened, pass the session ID down via your own state and read the useChatSessions hook.
Support
Bugs / feature requests — open a ticket in the TAI Jira project.
Internal questions —
#taion Slack.
