@astralbeam/sdk 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -64,26 +64,26 @@ export const POST = createAstralBeamTokenRoute({
64
64
  - Tokens use the API key's organization slug as issuer and the platform audience `astralbeam`; AstralBeam does not require or interpret `sub`.
65
65
  - Tokens are signed, not encrypted: never put a secret in them.
66
66
  - Lifetimes are 60–600 seconds; the SDK renews in memory before expiry.
67
- - The widget POSTs `authTokenUrl` with the page's cookies; pass `authTokenHeaders` (an object, or a function for a rotating credential) when your backend is on another origin behind header auth.
67
+ - `generateAuthToken` says where the token comes from: `{ url, ...init }`, which the widget calls as `fetch(url, init)` with a standard `RequestInit`, or a function returning `{ token }` (or a promise of it, or `undefined` when it cannot mint one).
68
+ - It defaults to `{ url: "/api/astralbeam/token" }`, posted with the page's cookies, and runs again on every renewal, so a rotating credential stays current.
68
69
 
69
70
  ## Options
70
71
 
71
- Every option is also a prop on `<AstralBeamChat>`; `handle.update(options)` applies any subset in place. `agentId`, `apiUrl`, `authTokenUrl`, and `authTokenHeaders` are fixed at mount. Details in [Configuration](https://app.astralbeam.ai/docs/sdk/configuration).
72
-
73
- | Option | Default | Meaning |
74
- | ------------------------------------ | ------------------------------- | --------------------------------------------------------------- |
75
- | `agentId` | organization's default | `agt_<organization>_<agent>` from the dashboard |
76
- | `apiUrl` | `https://app.astralbeam.ai/api` | Base URL of the AstralBeam API; the widget calls `/chat` there |
77
- | `authTokenUrl` | `/api/astralbeam/token` | Your token endpoint |
78
- | `authTokenHeaders` | none | Extra token-request headers, for a backend on another origin |
79
- | `title`, `showHeader` | `"AstralBeam"`, `true` | Header text, and whether the header and reset button show |
80
- | `emptyTitle`, `emptyDescription` | generic copy | Headline and subtitle of the empty transcript |
81
- | `colorScheme`, `theme` | `"system"`, built-in palette | Light/dark/system, and shadcn token overrides |
82
- | `attachments` | `true` | `false` hides the feature, or pass limits |
83
- | `tools`, `widgets` | none | What the agent can do and draw in your app |
84
- | `sandboxPanel` | `false` | Collected sandbox panel: files with downloads, command log |
85
- | `header`, `empty`, `composerActions` | widget's own chrome | Host-rendered replacements (React props; `slots` on the handle) |
86
- | `debug` | `false` | Log every SDK action in the browser and on the server |
72
+ Every option is also a prop on `<AstralBeamChat>`; `handle.update(options)` applies any subset in place, and no option is fixed at mount. Details in [Configuration](https://app.astralbeam.ai/docs/sdk/configuration).
73
+
74
+ | Option | Default | Meaning |
75
+ | ------------------------------------ | ---------------------------------- | ------------------------------------------------------------------ |
76
+ | `agentId` | organization's default | `agt_<organization>_<agent>` from the dashboard |
77
+ | `apiUrl` | `https://app.astralbeam.ai/api` | Base URL of the AstralBeam API; the widget calls `/chat` there |
78
+ | `generateAuthToken` | `{ url: "/api/astralbeam/token" }` | Token endpoint as `{ url, ...RequestInit }`, or a minting function |
79
+ | `title`, `showHeader` | `"AstralBeam"`, `true` | Header text, and whether the header and reset button show |
80
+ | `emptyTitle`, `emptyDescription` | generic copy | Headline and subtitle of the empty transcript |
81
+ | `colorScheme`, `theme` | `"system"`, built-in palette | Light/dark/system, and shadcn token overrides |
82
+ | `attachments` | `true` | `false` hides the feature, or pass limits |
83
+ | `tools`, `widgets` | none | What the agent can do and draw in your app |
84
+ | `sandboxPanel` | `false` | Collected sandbox panel: files with downloads, command log |
85
+ | `header`, `empty`, `composerActions` | widget's own chrome | Host-rendered replacements (React props; `slots` on the handle) |
86
+ | `debug` | `false` | Log every SDK action in the browser and on the server |
87
87
 
88
88
  A `ref` on `<AstralBeamChat>` (and the vanilla handle) exposes `reset()` and `stop()` for hosts that draw their own controls.
89
89
 
package/dist/client.d.ts CHANGED
@@ -99,10 +99,27 @@ type AstralBeamChatColorScheme = "light" | "dark" | "system";
99
99
  /** Overrides for the widget's theming CSS variables, keyed by custom-property name (`"--primary"`). */
100
100
  type AstralBeamChatThemeVariables = Record<`--${string}`, string>;
101
101
  /**
102
- * Headers added to the token request: an object for a fixed credential, or a function for one the
103
- * host must read or await per request, such as a rotating access token.
102
+ * A token endpoint to call: `{ url, ...init }`, which the widget calls as `fetch(url, init)` with
103
+ * this object's remaining, standard `RequestInit` fields. The init defaults to `POST`,
104
+ * `credentials: "include"`, `cache: "no-store"`, and an `accept: application/json` header, each
105
+ * overridable here, and the response is expected to carry `{ token }` as JSON.
104
106
  */
105
- type AstralBeamChatAuthTokenHeaders = Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
107
+ interface AstralBeamChatAuthTokenRequest extends RequestInit {
108
+ url: string;
109
+ }
110
+ /**
111
+ * Where the widget's short-lived chat JWT comes from: an endpoint to POST, or a function that
112
+ * mints the token in the host page and returns `{ token }`, optionally as a promise.
113
+ *
114
+ * Either form runs again on every renewal — near expiry and after a token is rejected — so a
115
+ * rotating credential stays current rather than being captured once. A function that returns
116
+ * `undefined`, or throws, fails authentication closed; the composer's retry link asks again.
117
+ */
118
+ type AstralBeamChatGenerateAuthToken = AstralBeamChatAuthTokenRequest | (() => {
119
+ token: string;
120
+ } | undefined | Promise<{
121
+ token: string;
122
+ } | undefined>);
106
123
  /**
107
124
  * Custom values for the CSS variables the widget's shadcn theme exposes (`--background`,
108
125
  * `--primary`, `--radius`, and the `--font-sans`/`--font-heading`/`--font-mono` stacks, ...),
@@ -115,8 +132,9 @@ interface AstralBeamChatTheme {
115
132
  }
116
133
  interface MountAstralBeamChatOptions {
117
134
  /**
118
- * Public ID of the organization-owned agent, fixed for this mounted chat. Omit it to use the
119
- * organization's default agent, which the dashboard's agents page selects.
135
+ * Public ID of the organization-owned agent. Omit it to use the organization's default agent,
136
+ * which the dashboard's agents page selects. A change answers the next run with the new agent
137
+ * and keeps the transcript, which that agent then sees as history.
120
138
  */
121
139
  agentId?: string | undefined;
122
140
  /** Name shown in the widget's header. Default `"AstralBeam"`. */
@@ -131,20 +149,18 @@ interface MountAstralBeamChatOptions {
131
149
  /** Subtitle shown under the empty transcript's headline. Default describes the app's tools and widgets. */
132
150
  emptyDescription?: string | undefined;
133
151
  /**
134
- * Base URL of the AstralBeam API; the widget calls `/chat` and its subroutes under it. Fixed at
135
- * mount. Default `"https://app.astralbeam.ai/api"`, the hosted cloud; self-hosted deployments
136
- * must set their own origin.
152
+ * Base URL of the AstralBeam API; the widget calls `/chat` and its subroutes under it. Read for
153
+ * every request, so a change moves the next one. Default `"https://app.astralbeam.ai/api"`, the
154
+ * hosted cloud; self-hosted deployments must set their own origin.
137
155
  */
138
156
  apiUrl?: string | undefined;
139
- /** Application endpoint that mints a short-lived chat JWT. Fixed at mount. Default `"/api/astralbeam/token"`. */
140
- authTokenUrl?: string | undefined;
141
157
  /**
142
- * Extra request headers for `authTokenUrl`, for hosts whose backend sits on another origin and
143
- * authenticates with a bearer token or a custom header rather than the page's cookies. Resolved
144
- * again on every token request, so pass the function form for a credential that rotates.
145
- * Fixed at mount.
158
+ * Where the short-lived chat JWT comes from: `{ url, ...RequestInit }` for a token endpoint, or
159
+ * a function that mints `{ token }` in the host page. Read for every token, so a change applies
160
+ * to the next one, which is minted when the cached token nears expiry. Default
161
+ * `{ url: "/api/astralbeam/token" }`, posted with the page's cookies.
146
162
  */
147
- authTokenHeaders?: AstralBeamChatAuthTokenHeaders | undefined;
163
+ generateAuthToken?: AstralBeamChatGenerateAuthToken | undefined;
148
164
  /** Host-defined tools the agent can call, executed in the host page, keyed by tool name. */
149
165
  tools?: Record<string, ToolDefinition> | undefined;
150
166
  /** Host-defined widgets the agent can render inline in the conversation, keyed by identifier. */
@@ -173,10 +189,11 @@ interface MountAstralBeamChatOptions {
173
189
  debug?: boolean | undefined;
174
190
  }
175
191
  /**
176
- * Mount options the handle can change afterwards. The agent and the transport URLs are fixed:
177
- * changing any of them would mean a new client and a discarded transcript.
192
+ * What the handle's `update` takes: every mount option, none of them fixed. The transport options
193
+ * are re-read per request rather than captured, so changing them keeps the transcript and the
194
+ * chat session instead of forcing a fresh mount.
178
195
  */
179
- type AstralBeamChatUpdate = Partial<Omit<MountAstralBeamChatOptions, "agentId" | "apiUrl" | "authTokenUrl" | "authTokenHeaders">>;
196
+ type AstralBeamChatUpdate = Partial<MountAstralBeamChatOptions>;
180
197
  interface AstralBeamChatHandle {
181
198
  unmount: () => void;
182
199
  /**
@@ -210,4 +227,4 @@ declare function defineWidget<const S extends ParametersSchema = JsonSchemaObjec
210
227
  //#region src/client/index.d.ts
211
228
  declare function mountAstralBeamChat(target: HTMLElement, options: MountAstralBeamChatOptions): AstralBeamChatHandle;
212
229
  //#endregion
213
- export { type AstralBeamChatAttachmentOptions, type AstralBeamChatAuthTokenHeaders, type AstralBeamChatColorScheme, type AstralBeamChatHandle, type AstralBeamChatSlotRenderer, type AstralBeamChatSlots, type AstralBeamChatTheme, type AstralBeamChatThemeVariables, type AstralBeamChatUpdate, type InferParameters, type JsonSchemaObject, type MountAstralBeamChatOptions, type ParametersSchema, type StandardSchemaV1, type ToolDefinition, type TypedToolDefinition, type TypedWidgetDefinition, type WidgetDefinition, defineTool, defineWidget, mountAstralBeamChat };
230
+ export { type AstralBeamChatAttachmentOptions, type AstralBeamChatAuthTokenRequest, type AstralBeamChatColorScheme, type AstralBeamChatGenerateAuthToken, type AstralBeamChatHandle, type AstralBeamChatSlotRenderer, type AstralBeamChatSlots, type AstralBeamChatTheme, type AstralBeamChatThemeVariables, type AstralBeamChatUpdate, type InferParameters, type JsonSchemaObject, type MountAstralBeamChatOptions, type ParametersSchema, type StandardSchemaV1, type ToolDefinition, type TypedToolDefinition, type TypedWidgetDefinition, type WidgetDefinition, defineTool, defineWidget, mountAstralBeamChat };
package/dist/client.js CHANGED
@@ -1 +1 @@
1
- import{c as e,t}from"./debug-DyjRg3e0.js";function n(e){return e}function r(e){return e}function i(n,r){let i={...r},a=t(i.debug);a?.(`mount`,`mounting chat widget`,{agentId:i.agentId??`(organization default)`,title:i.title??`AstralBeam`,showHeader:i.showHeader??!0,emptyTitle:i.emptyTitle??`Ask the assistant`,emptyDescription:i.emptyDescription??`It can answer questions and act through this app's own tools and widgets.`,apiUrl:i.apiUrl??`https://app.astralbeam.ai/api`,authentication:`configured`,colorScheme:i.colorScheme??`system`,theme:i.theme,tools:Object.keys(i.tools??{}),widgets:Object.keys(i.widgets??{}),attachments:i.attachments??!0});let o=n.shadowRoot??n.attachShadow({mode:`open`}),s=document.createElement(`div`);s.className=e,s.style.height=`100%`,o.append(s);let c=matchMedia(`(prefers-color-scheme: dark)`),l=new Set,u=e=>{for(let e of l)s.style.removeProperty(e);l.clear();let t={...i.theme?.light,...e?i.theme?.dark:void 0};for(let[e,n]of Object.entries(t))e.startsWith(`--`)&&(s.style.setProperty(e,n),l.add(e))},d=()=>{let e=i.colorScheme??`system`,t=e===`dark`||e===`system`&&c.matches;s.classList.toggle(`dark`,t),u(t),a?.(`theme`,`color scheme "${e}" resolved to ${t?`dark`:`light`}`,{themeVariables:[...l]})};d(),c.addEventListener(`change`,d);let f=!1,p;return import(`./widget-C6NW94JW.js`).then(({renderChat:e})=>{a?.(`mount`,`chat chunk loaded`),f||(p=e(o,s,i))}),{update:e=>{if(Object.hasOwn(e,`agentId`)||Object.hasOwn(e,`apiUrl`)||Object.hasOwn(e,`authTokenUrl`)||Object.hasOwn(e,`authTokenHeaders`))throw Error(`agentId, apiUrl, authTokenUrl, and authTokenHeaders are fixed at mount`);i={...i,...e},a=t(i.debug),a?.(`mount`,`options updated`,{changed:Object.keys(e)}),d(),p?.update(i)},reset:()=>p?.reset(),stop:()=>p?.stop(),unmount:()=>{a?.(`mount`,`unmounting chat widget`),f=!0,c.removeEventListener(`change`,d),p?.dispose(),p=void 0,s.remove()}}}export{n as defineTool,r as defineWidget,i as mountAstralBeamChat};
1
+ import{c as e,t}from"./debug-DyjRg3e0.js";function n(e){return e}function r(e){return e}function i(n,r){let i={...r},a=t(i.debug);a?.(`mount`,`mounting chat widget`,{agentId:i.agentId??`(organization default)`,title:i.title??`AstralBeam`,showHeader:i.showHeader??!0,emptyTitle:i.emptyTitle??`Ask the assistant`,emptyDescription:i.emptyDescription??`It can answer questions and act through this app's own tools and widgets.`,apiUrl:i.apiUrl??`https://app.astralbeam.ai/api`,authentication:`configured`,colorScheme:i.colorScheme??`system`,theme:i.theme,tools:Object.keys(i.tools??{}),widgets:Object.keys(i.widgets??{}),attachments:i.attachments??!0});let o=n.shadowRoot??n.attachShadow({mode:`open`}),s=document.createElement(`div`);s.className=e,s.style.height=`100%`,o.append(s);let c=matchMedia(`(prefers-color-scheme: dark)`),l=new Set,u=e=>{for(let e of l)s.style.removeProperty(e);l.clear();let t={...i.theme?.light,...e?i.theme?.dark:void 0};for(let[e,n]of Object.entries(t))e.startsWith(`--`)&&(s.style.setProperty(e,n),l.add(e))},d=()=>{let e=i.colorScheme??`system`,t=e===`dark`||e===`system`&&c.matches;s.classList.toggle(`dark`,t),u(t),a?.(`theme`,`color scheme "${e}" resolved to ${t?`dark`:`light`}`,{themeVariables:[...l]})};d(),c.addEventListener(`change`,d);let f=!1,p;return import(`./widget-DiGFLZyX.js`).then(({renderChat:e})=>{a?.(`mount`,`chat chunk loaded`),f||(p=e(o,s,i))}),{update:e=>{i={...i,...e},a=t(i.debug),a?.(`mount`,`options updated`,{changed:Object.keys(e)}),d(),p?.update(i)},reset:()=>p?.reset(),stop:()=>p?.stop(),unmount:()=>{a?.(`mount`,`unmounting chat widget`),f=!0,c.removeEventListener(`change`,d),p?.dispose(),p=void 0,s.remove()}}}export{n as defineTool,r as defineWidget,i as mountAstralBeamChat};
@@ -7861,19 +7861,19 @@ function bearerToken(headers) {
7861
7861
  const authorization = headers.get("authorization");
7862
7862
  return authorization?.startsWith("Bearer ") ? authorization.slice(7) : void 0;
7863
7863
  }
7864
- async function postChatToken(options, signal) {
7865
- const { authTokenUrl, authTokenHeaders, fetchClient } = options;
7866
- const headers = new Headers({ accept: "application/json" });
7867
- if (authTokenHeaders) {
7868
- const extra = typeof authTokenHeaders === "function" ? await authTokenHeaders() : authTokenHeaders;
7869
- for (const [name, value] of Object.entries(extra)) headers.set(name, value);
7870
- }
7871
- const response = await fetchClient(authTokenUrl, {
7864
+ async function requestChatToken(options, signal) {
7865
+ const { generateAuthToken, fetchClient } = options;
7866
+ if (typeof generateAuthToken === "function") return (await generateAuthToken())?.token;
7867
+ const { url, ...init } = generateAuthToken;
7868
+ const headers = new Headers(init.headers);
7869
+ if (!headers.has("accept")) headers.set("accept", "application/json");
7870
+ const response = await fetchClient(url, {
7872
7871
  method: "POST",
7873
- headers,
7874
7872
  credentials: "include",
7875
7873
  cache: "no-store",
7876
- signal
7874
+ ...init,
7875
+ headers,
7876
+ signal: init.signal ? AbortSignal.any([signal, init.signal]) : signal
7877
7877
  });
7878
7878
  if (!response.ok) throw new Error(`Authentication endpoint returned HTTP ${response.status}`);
7879
7879
  return (await response.json())?.token;
@@ -7881,11 +7881,12 @@ async function postChatToken(options, signal) {
7881
7881
  async function fetchChatToken(options) {
7882
7882
  const { session, onStateChange, debug } = options;
7883
7883
  const { signal } = session.abortController;
7884
+ const source = typeof options.generateAuthToken === "function" ? "generateAuthToken" : "Authentication endpoint";
7884
7885
  try {
7885
- const token = await postChatToken(options, signal);
7886
- if (typeof token !== "string" || !token) throw new Error("Authentication endpoint did not return a token");
7886
+ const token = await requestChatToken(options, signal);
7887
+ if (typeof token !== "string" || !token) throw new Error(`${source} did not return a token`);
7887
7888
  const expiresAt = tokenExpiry(token);
7888
- if (expiresAt <= Date.now()) throw new Error("Authentication endpoint returned an expired token");
7889
+ if (expiresAt <= Date.now()) throw new Error(`${source} returned an expired token`);
7889
7890
  session.cached = {
7890
7891
  value: token,
7891
7892
  expiresAt
@@ -8081,9 +8082,8 @@ function describeSandboxCommandRun(run) {
8081
8082
  * whole UI is another.
8082
8083
  */
8083
8084
  function createAstralBeamChat(options) {
8084
- const debug = createDebugLogger(options.debug);
8085
- const urls = chatApiUrls(options.apiUrl);
8086
- const widgets = options.widgets ?? {};
8085
+ let live = { ...options };
8086
+ let debug = createDebugLogger(live.debug);
8087
8087
  const listeners = /* @__PURE__ */ new Set();
8088
8088
  let state = {
8089
8089
  messages: [],
@@ -8105,8 +8105,7 @@ function createAstralBeamChat(options) {
8105
8105
  for (const listener of listeners) listener();
8106
8106
  };
8107
8107
  const authentication = {
8108
- authTokenUrl: options.authTokenUrl ?? "/api/astralbeam/token",
8109
- authTokenHeaders: options.authTokenHeaders,
8108
+ generateAuthToken: live.generateAuthToken ?? { url: "/api/astralbeam/token" },
8110
8109
  session: {
8111
8110
  cached: void 0,
8112
8111
  refreshPromise: void 0,
@@ -8117,10 +8116,10 @@ function createAstralBeamChat(options) {
8117
8116
  debug
8118
8117
  };
8119
8118
  initializeChatAuthentication(authentication).catch(() => void 0);
8120
- (async () => {
8119
+ const resolveCapabilities = async () => {
8121
8120
  try {
8122
- const url = new URL(urls.config, globalThis.location?.href);
8123
- if (options.agentId) url.searchParams.set("agentId", options.agentId);
8121
+ const url = new URL(chatApiUrls(live.apiUrl).config, globalThis.location?.href);
8122
+ if (live.agentId) url.searchParams.set("agentId", live.agentId);
8124
8123
  const token = await getValidChatToken(authentication);
8125
8124
  const response = await fetch(url, { headers: { authorization: `Bearer ${token}` } });
8126
8125
  if (!response.ok) throw new Error(`The config request answered ${response.status}`);
@@ -8129,20 +8128,22 @@ function createAstralBeamChat(options) {
8129
8128
  } catch (error) {
8130
8129
  debug?.("error", "agent capabilities could not be resolved; keeping the defaults", error);
8131
8130
  }
8132
- })();
8131
+ };
8132
+ resolveCapabilities();
8133
8133
  const renderCleanups = /* @__PURE__ */ new Map();
8134
8134
  const disposeRenders = () => {
8135
8135
  for (const cleanup of renderCleanups.values()) cleanup();
8136
8136
  renderCleanups.clear();
8137
8137
  };
8138
8138
  const renderWidget = async (input, toolCallId) => {
8139
+ const widgets = live.widgets ?? {};
8139
8140
  if (!Object.hasOwn(widgets, input.widget)) throw new Error(`Unknown widget "${input.widget}"`);
8140
8141
  const declaration = widgets[input.widget];
8141
8142
  const validated = await validateParameters(declaration?.parameters, input.props ?? {});
8142
8143
  if (validated == null) throw new Error(`Props for widget "${input.widget}" failed validation`);
8143
8144
  renderCleanups.get(toolCallId)?.();
8144
8145
  renderCleanups.delete(toolCallId);
8145
- const cleanup = options.onRenderWidget?.({
8146
+ const cleanup = live.onRenderWidget?.({
8146
8147
  widget: input.widget,
8147
8148
  props: validated,
8148
8149
  toolCallId
@@ -8150,11 +8151,16 @@ function createAstralBeamChat(options) {
8150
8151
  if (cleanup) renderCleanups.set(toolCallId, cleanup);
8151
8152
  return {
8152
8153
  widget: input.widget,
8153
- rendered: options.onRenderWidget !== void 0
8154
+ rendered: live.onRenderWidget !== void 0
8154
8155
  };
8155
8156
  };
8157
+ const agentTools = () => buildAgentTools(live.widgets ?? {}, live.tools ?? {}, renderWidget, debug);
8158
+ const forwardedProps = () => ({
8159
+ ...live.agentId ? { agentId: live.agentId } : {},
8160
+ ...live.debug ? { debug: true } : {}
8161
+ });
8156
8162
  const client = new ChatClient({
8157
- connection: fetchServerSentEvents(urls.chat, async () => ({
8163
+ connection: fetchServerSentEvents(() => chatApiUrls(live.apiUrl).chat, async () => ({
8158
8164
  headers: { authorization: `Bearer ${await getValidChatToken(authentication)}` },
8159
8165
  fetchClient: (input, init) => fetchAuthenticatedChat({
8160
8166
  ...authentication,
@@ -8162,11 +8168,8 @@ function createAstralBeamChat(options) {
8162
8168
  init
8163
8169
  })
8164
8170
  })),
8165
- tools: buildAgentTools(widgets, options.tools ?? {}, renderWidget, debug),
8166
- forwardedProps: {
8167
- ...options.agentId ? { agentId: options.agentId } : {},
8168
- ...options.debug ? { debug: true } : {}
8169
- },
8171
+ tools: agentTools(),
8172
+ forwardedProps: forwardedProps(),
8170
8173
  onMessagesChange: (messages) => {
8171
8174
  update({
8172
8175
  messages,
@@ -8213,6 +8216,22 @@ function createAstralBeamChat(options) {
8213
8216
  listeners.add(listener);
8214
8217
  return () => listeners.delete(listener);
8215
8218
  },
8219
+ updateOptions: (next) => {
8220
+ const agent = live.agentId;
8221
+ const apiUrl = live.apiUrl;
8222
+ live = {
8223
+ ...live,
8224
+ ...next
8225
+ };
8226
+ debug = createDebugLogger(live.debug);
8227
+ authentication.generateAuthToken = live.generateAuthToken ?? { url: "/api/astralbeam/token" };
8228
+ authentication.debug = debug;
8229
+ client.updateOptions({
8230
+ tools: agentTools(),
8231
+ forwardedProps: forwardedProps()
8232
+ });
8233
+ if (live.agentId !== agent || live.apiUrl !== apiUrl) resolveCapabilities();
8234
+ },
8216
8235
  sendMessage: (content) => {
8217
8236
  settleDanglingToolCalls();
8218
8237
  return client.sendMessage(content);
package/dist/core.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { A as SandboxActivity, C as AstralBeamChatState, D as WidgetDeclaration, E as ChatAuthenticationState, F as InferParameters, I as JsonSchemaObject, L as ParametersSchema, M as SandboxCommandRun, N as SandboxFileWrite, O as buildAgentTools, P as SandboxStatus, R as StandardSchemaV1, S as AstralBeamChatCoreOptions, T as createAstralBeamChat, _ as sandboxRefusal, a as SANDBOX_LIST_FILES_TOOL, b as lastPartInProgress, c as SANDBOX_RUN_COMMAND_TOOL, d as collectSandboxActivity, f as describeSandboxCommandRun, g as readSandboxFileWrite, h as readSandboxCommandRun, i as RENDER_WIDGET_TOOL, j as SandboxArtifact, k as RenderWidgetInput, l as SANDBOX_STATUS_EVENT, m as readSandboxArtifact, n as defineWidget, o as SANDBOX_PUBLISH_ARTIFACT_TOOL, p as isSandboxTool, r as ASK_QUESTIONNAIRE_TOOL, s as SANDBOX_READ_FILE_TOOL, t as defineTool, u as SANDBOX_WRITE_FILE_TOOL, v as hasPendingToolRun, w as WidgetRenderRequest, x as AstralBeamChatCore, y as isSettledToolCall, z as ToolDefinition } from "./index-DJWIt_VP.js";
1
+ import { A as SandboxActivity, C as AstralBeamChatState, D as WidgetDeclaration, E as ChatAuthenticationState, F as InferParameters, I as JsonSchemaObject, L as ParametersSchema, M as SandboxCommandRun, N as SandboxFileWrite, O as buildAgentTools, P as SandboxStatus, R as StandardSchemaV1, S as AstralBeamChatCoreOptions, T as createAstralBeamChat, _ as sandboxRefusal, a as SANDBOX_LIST_FILES_TOOL, b as lastPartInProgress, c as SANDBOX_RUN_COMMAND_TOOL, d as collectSandboxActivity, f as describeSandboxCommandRun, g as readSandboxFileWrite, h as readSandboxCommandRun, i as RENDER_WIDGET_TOOL, j as SandboxArtifact, k as RenderWidgetInput, l as SANDBOX_STATUS_EVENT, m as readSandboxArtifact, n as defineWidget, o as SANDBOX_PUBLISH_ARTIFACT_TOOL, p as isSandboxTool, r as ASK_QUESTIONNAIRE_TOOL, s as SANDBOX_READ_FILE_TOOL, t as defineTool, u as SANDBOX_WRITE_FILE_TOOL, v as hasPendingToolRun, w as WidgetRenderRequest, x as AstralBeamChatCore, y as isSettledToolCall, z as ToolDefinition } from "./index-8NUgB59m.js";
2
2
  export { ASK_QUESTIONNAIRE_TOOL, type AstralBeamChatCore, type AstralBeamChatCoreOptions, type AstralBeamChatState, type ChatAuthenticationState, type InferParameters, type JsonSchemaObject, type ParametersSchema, RENDER_WIDGET_TOOL, type RenderWidgetInput, SANDBOX_LIST_FILES_TOOL, SANDBOX_PUBLISH_ARTIFACT_TOOL, SANDBOX_READ_FILE_TOOL, SANDBOX_RUN_COMMAND_TOOL, SANDBOX_STATUS_EVENT, SANDBOX_WRITE_FILE_TOOL, type SandboxActivity, type SandboxArtifact, type SandboxCommandRun, type SandboxFileWrite, type SandboxStatus, type StandardSchemaV1, type ToolDefinition, type WidgetDeclaration, type WidgetRenderRequest, buildAgentTools, collectSandboxActivity, createAstralBeamChat, defineTool, defineWidget, describeSandboxCommandRun, hasPendingToolRun, isSandboxTool, isSettledToolCall, lastPartInProgress, readSandboxArtifact, readSandboxCommandRun, readSandboxFileWrite, sandboxRefusal };
package/dist/core.js CHANGED
@@ -1,2 +1,2 @@
1
- import { S as SANDBOX_WRITE_FILE_TOOL, _ as SANDBOX_LIST_FILES_TOOL, a as describeSandboxCommandRun, b as SANDBOX_RUN_COMMAND_TOOL, c as readSandboxCommandRun, d as hasPendingToolRun, f as isSettledToolCall, g as RENDER_WIDGET_TOOL, h as ASK_QUESTIONNAIRE_TOOL, i as collectSandboxActivity, l as readSandboxFileWrite, m as buildAgentTools, n as defineWidget, o as isSandboxTool, p as lastPartInProgress, r as createAstralBeamChat, s as readSandboxArtifact, t as defineTool, u as sandboxRefusal, v as SANDBOX_PUBLISH_ARTIFACT_TOOL, x as SANDBOX_STATUS_EVENT, y as SANDBOX_READ_FILE_TOOL } from "./core-2LtbAqiQ.js";
1
+ import { S as SANDBOX_WRITE_FILE_TOOL, _ as SANDBOX_LIST_FILES_TOOL, a as describeSandboxCommandRun, b as SANDBOX_RUN_COMMAND_TOOL, c as readSandboxCommandRun, d as hasPendingToolRun, f as isSettledToolCall, g as RENDER_WIDGET_TOOL, h as ASK_QUESTIONNAIRE_TOOL, i as collectSandboxActivity, l as readSandboxFileWrite, m as buildAgentTools, n as defineWidget, o as isSandboxTool, p as lastPartInProgress, r as createAstralBeamChat, s as readSandboxArtifact, t as defineTool, u as sandboxRefusal, v as SANDBOX_PUBLISH_ARTIFACT_TOOL, x as SANDBOX_STATUS_EVENT, y as SANDBOX_READ_FILE_TOOL } from "./core-BZxnUlwg.js";
2
2
  export { ASK_QUESTIONNAIRE_TOOL, RENDER_WIDGET_TOOL, SANDBOX_LIST_FILES_TOOL, SANDBOX_PUBLISH_ARTIFACT_TOOL, SANDBOX_READ_FILE_TOOL, SANDBOX_RUN_COMMAND_TOOL, SANDBOX_STATUS_EVENT, SANDBOX_WRITE_FILE_TOOL, buildAgentTools, collectSandboxActivity, createAstralBeamChat, defineTool, defineWidget, describeSandboxCommandRun, hasPendingToolRun, isSandboxTool, isSettledToolCall, lastPartInProgress, readSandboxArtifact, readSandboxCommandRun, readSandboxFileWrite, sandboxRefusal };
@@ -3306,10 +3306,27 @@ interface ToolDefinition {
3306
3306
  execute: (input: Record<string, unknown>) => unknown | Promise<unknown>;
3307
3307
  }
3308
3308
  /**
3309
- * Headers added to the token request: an object for a fixed credential, or a function for one the
3310
- * host must read or await per request, such as a rotating access token.
3309
+ * A token endpoint to call: `{ url, ...init }`, which the widget calls as `fetch(url, init)` with
3310
+ * this object's remaining, standard `RequestInit` fields. The init defaults to `POST`,
3311
+ * `credentials: "include"`, `cache: "no-store"`, and an `accept: application/json` header, each
3312
+ * overridable here, and the response is expected to carry `{ token }` as JSON.
3311
3313
  */
3312
- type AstralBeamChatAuthTokenHeaders = Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
3314
+ interface AstralBeamChatAuthTokenRequest extends RequestInit {
3315
+ url: string;
3316
+ }
3317
+ /**
3318
+ * Where the widget's short-lived chat JWT comes from: an endpoint to POST, or a function that
3319
+ * mints the token in the host page and returns `{ token }`, optionally as a promise.
3320
+ *
3321
+ * Either form runs again on every renewal — near expiry and after a token is rejected — so a
3322
+ * rotating credential stays current rather than being captured once. A function that returns
3323
+ * `undefined`, or throws, fails authentication closed; the composer's retry link asks again.
3324
+ */
3325
+ type AstralBeamChatGenerateAuthToken = AstralBeamChatAuthTokenRequest | (() => {
3326
+ token: string;
3327
+ } | undefined | Promise<{
3328
+ token: string;
3329
+ } | undefined>);
3313
3330
  //#endregion
3314
3331
  //#region src/lib/debug.d.ts
3315
3332
  declare const CATEGORY_COLORS: {
@@ -3597,14 +3614,12 @@ interface AstralBeamChatCoreOptions {
3597
3614
  agentId?: string | undefined;
3598
3615
  /** Base URL of the AstralBeam API; `/chat` hangs off it. Default the hosted cloud. */
3599
3616
  apiUrl?: string | undefined;
3600
- /** The application endpoint that mints short-lived chat JWTs. Default `/api/astralbeam/token`. */
3601
- authTokenUrl?: string | undefined;
3602
3617
  /**
3603
- * Extra headers for the token request, for a backend on another origin that authenticates with a
3604
- * bearer token or custom header instead of cookies. Resolved on every token request, so pass the
3605
- * function form for a credential that rotates.
3618
+ * Where short-lived chat JWTs come from: `{ url, ...RequestInit }` for a token endpoint, or a
3619
+ * function minting `{ token }` in the host page. Either runs again on every renewal.
3620
+ * Default `{ url: "/api/astralbeam/token" }`.
3606
3621
  */
3607
- authTokenHeaders?: AstralBeamChatAuthTokenHeaders | undefined;
3622
+ generateAuthToken?: AstralBeamChatGenerateAuthToken | undefined;
3608
3623
  /** Host tools the agent can call; `execute` runs wherever this session lives. */
3609
3624
  tools?: Record<string, ToolDefinition> | undefined;
3610
3625
  /** Widgets declared to the agent; `onRenderWidget` is asked to draw them. */
@@ -3631,6 +3646,11 @@ interface AstralBeamChatCore {
3631
3646
  getState: () => AstralBeamChatState;
3632
3647
  /** Notifies on every state change; returns the unsubscribe. */
3633
3648
  subscribe: (listener: () => void) => () => void;
3649
+ /**
3650
+ * Merges option changes into the session and applies them in place, keeping the transcript and
3651
+ * the chat session. Only the keys given are replaced.
3652
+ */
3653
+ updateOptions: (options: Partial<AstralBeamChatCoreOptions>) => void;
3634
3654
  /** Sends a message, first settling any dangling tool calls so the run can proceed. */
3635
3655
  sendMessage: (content: string | MultimodalContent) => Promise<void>;
3636
3656
  /** Resolves a client tool call the host executed itself (a questionnaire, an approval). */
package/dist/react.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { C as AstralBeamChatState, S as AstralBeamChatCoreOptions, x as AstralBeamChatCore } from "./index-DJWIt_VP.js";
1
+ import { C as AstralBeamChatState, S as AstralBeamChatCoreOptions, x as AstralBeamChatCore } from "./index-8NUgB59m.js";
2
2
  import { ReactNode } from "react";
3
- import { AstralBeamChatAttachmentOptions, AstralBeamChatAuthTokenHeaders, AstralBeamChatColorScheme, AstralBeamChatTheme, InferParameters, JsonSchemaObject, ParametersSchema, ToolDefinition, WidgetDefinition as WidgetDefinition$1, defineTool } from "@astralbeam/sdk/client";
3
+ import { AstralBeamChatAttachmentOptions, AstralBeamChatAuthTokenRequest, AstralBeamChatColorScheme, AstralBeamChatGenerateAuthToken, AstralBeamChatTheme, InferParameters, JsonSchemaObject, ParametersSchema, ToolDefinition, WidgetDefinition as WidgetDefinition$1, defineTool } from "@astralbeam/sdk/client";
4
4
 
5
5
  //#region src/react/index.d.ts
6
6
  interface WidgetDefinition extends Omit<WidgetDefinition$1, "render"> {
@@ -26,11 +26,9 @@ interface UseAstralBeamChatResult extends AstralBeamChatState {
26
26
  }
27
27
  /**
28
28
  * The headless chat session as a React hook: authentication, transport, tools, and transcript
29
- * state with no markup, for hosts that own their whole chat UI. Transport identity (endpoints,
30
- * agent) and the declared tool/widget set are fixed for the component's lifetime remount with
31
- * a React `key` to change them — but `execute` and `onRenderWidget` read the latest render, so
32
- * ordinary closures over props and state stay live. Whether a widget renderer exists at all is
33
- * part of the declared surface and is read at mount.
29
+ * state with no markup, for hosts that own their whole chat UI. Every option follows the props it
30
+ * is given, keeping the transcript and the chat session, so ordinary closures over props and
31
+ * state stay live and nothing needs a remount.
34
32
  */
35
33
  declare function useAstralBeamChat(options: AstralBeamChatCoreOptions): UseAstralBeamChatResult;
36
34
  /** Imperative surface of a mounted `<AstralBeamChat>`, for hosts that draw their own controls. */
@@ -42,15 +40,16 @@ interface AstralBeamChatRef {
42
40
  }
43
41
  interface AstralBeamChatProps {
44
42
  /**
45
- * Public ID of the organization-owned agent, fixed for this mounted chat. Omit it to use the
46
- * organization's default agent, which the dashboard's agents page selects.
43
+ * Public ID of the organization-owned agent. Omit it to use the organization's default agent,
44
+ * which the dashboard's agents page selects. A change answers the next run with the new agent
45
+ * and keeps the transcript, which that agent then sees as history.
47
46
  */
48
47
  agentId?: string;
49
- /** Name shown in the widget's header; prop changes apply immediately. Default `"AstralBeam"`. */
48
+ /** Name shown in the widget's header. Default `"AstralBeam"`. */
50
49
  title?: string;
51
50
  /**
52
51
  * Shows the widget's header with the title and the reset button; `false` hides both and gives
53
- * the transcript the full height. Prop changes apply immediately. Default `true`.
52
+ * the transcript the full height. Default `true`.
54
53
  */
55
54
  showHeader?: boolean;
56
55
  /** Replaces the header's content with the host's own React content; `showHeader` still applies. */
@@ -59,28 +58,29 @@ interface AstralBeamChatProps {
59
58
  empty?: ReactNode;
60
59
  /** Extra host controls at the end of the composer's button row, next to send. */
61
60
  composerActions?: ReactNode;
62
- /** Headline shown on the empty transcript; prop changes apply immediately. Default `"Ask the assistant"`. */
61
+ /** Headline shown on the empty transcript. Default `"Ask the assistant"`. */
63
62
  emptyTitle?: string;
64
- /** Subtitle under the empty transcript's headline; prop changes apply immediately. */
63
+ /** Subtitle under the empty transcript's headline. */
65
64
  emptyDescription?: string;
66
- /** Base URL of the AstralBeam API; the widget calls `/chat` under it. Default the hosted cloud. */
65
+ /**
66
+ * Base URL of the AstralBeam API; the widget calls `/chat` under it. Read per request, so a
67
+ * change moves the next one. Default the hosted cloud.
68
+ */
67
69
  apiUrl?: string;
68
- /** Application endpoint that mints a short-lived chat JWT. Default `"/api/astralbeam/token"`. */
69
- authTokenUrl?: string;
70
70
  /**
71
- * Extra headers for the token request, for a backend on another origin that authenticates with a
72
- * bearer token or custom header instead of cookies. Always read from the latest render, so an
73
- * inline object or callback over current auth state is fine, needs no memoization, and may
74
- * start out undefined while the host's own credential loads.
71
+ * Where the short-lived chat JWT comes from: `{ url, ...RequestInit }` for a token endpoint, or
72
+ * a function minting `{ token }`, optionally a promise, in the host app. Read per token, and
73
+ * the function form runs in the host's React tree, so an inline closure over current auth state
74
+ * is fine and needs no memoization. Default `{ url: "/api/astralbeam/token" }`.
75
75
  */
76
- authTokenHeaders?: AstralBeamChatAuthTokenHeaders;
76
+ generateAuthToken?: AstralBeamChatGenerateAuthToken;
77
77
  /** Host-defined tools the agent can call, executed in the host's React app, keyed by name. */
78
78
  tools?: Record<string, ToolDefinition>;
79
79
  /** Host-defined widgets the agent can render inline in the conversation, keyed by identifier. */
80
80
  widgets?: Record<string, WidgetDefinition>;
81
- /** Color scheme of the chat widget; prop changes apply immediately. Default `"system"`. */
81
+ /** Color scheme of the chat widget. Default `"system"`. */
82
82
  colorScheme?: AstralBeamChatColorScheme;
83
- /** Custom values for the widget's theming CSS variables, per color scheme; changes apply immediately. */
83
+ /** Custom values for the widget's theming CSS variables, per color scheme. */
84
84
  theme?: AstralBeamChatTheme | undefined;
85
85
  /** File attachments in the composer, on by default; `false` turns them off. */
86
86
  attachments?: boolean | AstralBeamChatAttachmentOptions;
@@ -88,10 +88,10 @@ interface AstralBeamChatProps {
88
88
  sandboxPanel?: boolean;
89
89
  /**
90
90
  * Logs every SDK action to the browser console with UTC timestamps and full payloads,
91
- * and asks the endpoint to log its side of the run too; prop changes apply immediately.
91
+ * and asks the endpoint to log its side of the run too.
92
92
  */
93
93
  debug?: boolean;
94
94
  }
95
95
  declare const AstralBeamChat: import("react").ForwardRefExoticComponent<AstralBeamChatProps & import("react").RefAttributes<AstralBeamChatRef>>;
96
96
  //#endregion
97
- export { AstralBeamChat, type AstralBeamChatAttachmentOptions, type AstralBeamChatAuthTokenHeaders, type AstralBeamChatColorScheme, type AstralBeamChatCore, type AstralBeamChatCoreOptions, AstralBeamChatProps, AstralBeamChatRef, type AstralBeamChatState, type AstralBeamChatTheme, type InferParameters, type ParametersSchema, type ToolDefinition, TypedReactWidgetDefinition, UseAstralBeamChatResult, WidgetDefinition, defineTool, defineWidget, useAstralBeamChat };
97
+ export { AstralBeamChat, type AstralBeamChatAttachmentOptions, type AstralBeamChatAuthTokenRequest, type AstralBeamChatColorScheme, type AstralBeamChatCore, type AstralBeamChatCoreOptions, type AstralBeamChatGenerateAuthToken, AstralBeamChatProps, AstralBeamChatRef, type AstralBeamChatState, type AstralBeamChatTheme, type InferParameters, type ParametersSchema, type ToolDefinition, TypedReactWidgetDefinition, UseAstralBeamChatResult, WidgetDefinition, defineTool, defineWidget, useAstralBeamChat };
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { C as DEFAULT_COLOR_SCHEME, r as createAstralBeamChat } from "./core-2LtbAqiQ.js";
1
+ import { C as DEFAULT_COLOR_SCHEME, r as createAstralBeamChat } from "./core-BZxnUlwg.js";
2
2
  import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState, useSyncExternalStore } from "react";
3
3
  import { createPortal } from "react-dom";
4
4
  import { defineTool, mountAstralBeamChat } from "@astralbeam/sdk/client";
@@ -10,27 +10,24 @@ function defineWidget(widget) {
10
10
  }
11
11
  /**
12
12
  * The headless chat session as a React hook: authentication, transport, tools, and transcript
13
- * state with no markup, for hosts that own their whole chat UI. Transport identity (endpoints,
14
- * agent) and the declared tool/widget set are fixed for the component's lifetime remount with
15
- * a React `key` to change them — but `execute` and `onRenderWidget` read the latest render, so
16
- * ordinary closures over props and state stay live. Whether a widget renderer exists at all is
17
- * part of the declared surface and is read at mount.
13
+ * state with no markup, for hosts that own their whole chat UI. Every option follows the props it
14
+ * is given, keeping the transcript and the chat session, so ordinary closures over props and
15
+ * state stay live and nothing needs a remount.
18
16
  */
19
17
  function useAstralBeamChat(options) {
20
- const optionsRef = useRef(options);
21
- optionsRef.current = options;
22
- const [core] = useState(() => createAstralBeamChat({
23
- ...options,
24
- tools: Object.fromEntries(Object.entries(options.tools ?? {}).map(([name, definition]) => [name, {
25
- ...definition,
26
- execute: (input) => {
27
- const current = optionsRef.current.tools?.[name];
28
- if (!current) throw new Error(`Tool "${name}" is no longer registered`);
29
- return current.execute(input);
30
- }
31
- }])),
32
- onRenderWidget: options.onRenderWidget === void 0 ? void 0 : (request) => optionsRef.current.onRenderWidget?.(request)
33
- }));
18
+ const [core] = useState(() => createAstralBeamChat(options));
19
+ useEffect(() => {
20
+ core.updateOptions(options);
21
+ }, [
22
+ core,
23
+ options.agentId,
24
+ options.apiUrl,
25
+ options.generateAuthToken,
26
+ options.tools,
27
+ options.widgets,
28
+ options.onRenderWidget,
29
+ options.debug
30
+ ]);
34
31
  useEffect(() => () => core.dispose(), [core]);
35
32
  return {
36
33
  ...useSyncExternalStore(core.subscribe, core.getState, core.getState),
@@ -42,7 +39,7 @@ function useAstralBeamChat(options) {
42
39
  core
43
40
  };
44
41
  }
45
- const AstralBeamChat = forwardRef(function AstralBeamChat({ agentId, title, showHeader, header, empty, composerActions, emptyTitle, emptyDescription, apiUrl, authTokenUrl, authTokenHeaders, tools, widgets = {}, colorScheme = DEFAULT_COLOR_SCHEME, theme, attachments, sandboxPanel, debug }, ref) {
42
+ const AstralBeamChat = forwardRef(function AstralBeamChat({ agentId, title, showHeader, header, empty, composerActions, emptyTitle, emptyDescription, apiUrl, generateAuthToken, tools, widgets = {}, colorScheme = DEFAULT_COLOR_SCHEME, theme, attachments, sandboxPanel, debug }, ref) {
46
43
  const targetRef = useRef(null);
47
44
  const handleRef = useRef(null);
48
45
  const [activeRenders, setActiveRenders] = useState(/* @__PURE__ */ new Map());
@@ -54,10 +51,6 @@ const AstralBeamChat = forwardRef(function AstralBeamChat({ agentId, title, show
54
51
  useEffect(() => {
55
52
  toolsRef.current = tools;
56
53
  });
57
- const authTokenHeadersRef = useRef(authTokenHeaders);
58
- useEffect(() => {
59
- authTokenHeadersRef.current = authTokenHeaders;
60
- });
61
54
  const nextRenderKey = useRef(0);
62
55
  const hostTools = useMemo(() => Object.fromEntries(Object.entries(tools ?? {}).map(([name, definition]) => [name, {
63
56
  ...definition,
@@ -111,6 +104,9 @@ const AstralBeamChat = forwardRef(function AstralBeamChat({ agentId, title, show
111
104
  hasComposerActions
112
105
  ]);
113
106
  const live = useMemo(() => ({
107
+ agentId,
108
+ apiUrl,
109
+ generateAuthToken,
114
110
  title,
115
111
  showHeader,
116
112
  emptyTitle,
@@ -124,6 +120,9 @@ const AstralBeamChat = forwardRef(function AstralBeamChat({ agentId, title, show
124
120
  widgets: hostWidgets,
125
121
  slots: chromeSlots
126
122
  }), [
123
+ agentId,
124
+ apiUrl,
125
+ generateAuthToken,
127
126
  title,
128
127
  showHeader,
129
128
  emptyTitle,
@@ -141,16 +140,7 @@ const AstralBeamChat = forwardRef(function AstralBeamChat({ agentId, title, show
141
140
  liveRef.current = live;
142
141
  useEffect(() => {
143
142
  if (!targetRef.current) return;
144
- const handle = mountAstralBeamChat(targetRef.current, {
145
- ...liveRef.current,
146
- agentId,
147
- apiUrl,
148
- authTokenUrl,
149
- authTokenHeaders: () => {
150
- const current = authTokenHeadersRef.current;
151
- return typeof current === "function" ? current() : current ?? {};
152
- }
153
- });
143
+ const handle = mountAstralBeamChat(targetRef.current, liveRef.current);
154
144
  handleRef.current = handle;
155
145
  return () => {
156
146
  handleRef.current = null;