@noodleseed/assistant 1.13.0 → 1.15.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.
@@ -1,14 +1,14 @@
1
- import { A as AssistantPageContext, a as AssistantContext, C as CreateAssistantClientOptions, b as AssistantChatState, c as AssistantClient } from '../client-BKIeuSVS.cjs';
1
+ import { A as AssistantPageContext, a as AssistantContext, C as CreateAssistantClientOptions, b as AssistantChatState, c as AssistantClient } from '../client-B_l59rWg.cjs';
2
2
  import '../appearance-Cc-eqSbp.cjs';
3
3
  import 'ai';
4
4
 
5
- interface UseNoodleAssistantOptions<TPageContext extends AssistantPageContext = AssistantContext> extends CreateAssistantClientOptions<TPageContext> {
5
+ type UseNoodleAssistantOptions<TPageContext extends AssistantPageContext = AssistantContext> = CreateAssistantClientOptions<TPageContext> & {
6
6
  /**
7
7
  * Browser-local identity for the authenticated principal/tenant.
8
8
  * Changing it discards the previous in-memory session and transcript; it is never sent to Noodle.
9
9
  */
10
10
  readonly principalKey: string;
11
- }
11
+ };
12
12
  type UseNoodleAssistantResult<TPageContext extends AssistantPageContext = AssistantContext> = AssistantChatState & {
13
13
  /** Canonical controller for turns, interactions, context, Apps requests, and aborts. */
14
14
  readonly client: AssistantClient<TPageContext>;
@@ -1,14 +1,14 @@
1
- import { A as AssistantPageContext, a as AssistantContext, C as CreateAssistantClientOptions, b as AssistantChatState, c as AssistantClient } from '../client-BccAX_om.js';
1
+ import { A as AssistantPageContext, a as AssistantContext, C as CreateAssistantClientOptions, b as AssistantChatState, c as AssistantClient } from '../client-MnLfiyo2.js';
2
2
  import '../appearance-Cc-eqSbp.js';
3
3
  import 'ai';
4
4
 
5
- interface UseNoodleAssistantOptions<TPageContext extends AssistantPageContext = AssistantContext> extends CreateAssistantClientOptions<TPageContext> {
5
+ type UseNoodleAssistantOptions<TPageContext extends AssistantPageContext = AssistantContext> = CreateAssistantClientOptions<TPageContext> & {
6
6
  /**
7
7
  * Browser-local identity for the authenticated principal/tenant.
8
8
  * Changing it discards the previous in-memory session and transcript; it is never sent to Noodle.
9
9
  */
10
10
  readonly principalKey: string;
11
- }
11
+ };
12
12
  type UseNoodleAssistantResult<TPageContext extends AssistantPageContext = AssistantContext> = AssistantChatState & {
13
13
  /** Canonical controller for turns, interactions, context, Apps requests, and aborts. */
14
14
  readonly client: AssistantClient<TPageContext>;
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  createAssistantClient
4
- } from "../chunk-KRB2SXFD.js";
4
+ } from "../chunk-RRJIK473.js";
5
5
  import "../chunk-3YYTUJPJ.js";
6
6
  import "../chunk-5FUTL2UF.js";
7
7
 
@@ -10,16 +10,23 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useSyncExternalStore } fro
10
10
  var useCommittedLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
11
11
  function useNoodleAssistant(options) {
12
12
  const optionsRef = useRef(options);
13
- const { fetch: injectedFetch, principalKey, sessionEndpoint } = options;
13
+ const { fetch: injectedFetch, principalKey, sessionEndpoint, embedId, serviceUrl } = options;
14
14
  const client = useMemo(() => {
15
15
  if (!principalKey.trim()) throw new Error("principalKey is required");
16
- return createAssistantClient({
17
- sessionEndpoint,
16
+ const behavior = {
18
17
  ...injectedFetch ? { fetch: injectedFetch } : {},
19
18
  clientContext: () => readProvider(optionsRef.current.clientContext),
20
19
  pageContext: () => readProvider(optionsRef.current.pageContext)
20
+ };
21
+ return embedId !== void 0 ? createAssistantClient({
22
+ embedId,
23
+ ...serviceUrl !== void 0 ? { serviceUrl } : {},
24
+ ...behavior
25
+ }) : createAssistantClient({
26
+ sessionEndpoint: sessionEndpoint ?? "",
27
+ ...behavior
21
28
  });
22
- }, [injectedFetch, principalKey, sessionEndpoint]);
29
+ }, [injectedFetch, principalKey, sessionEndpoint, embedId, serviceUrl]);
23
30
  const store = useMemo(() => createAssistantReactStore(client), [client]);
24
31
  const state = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
25
32
  const synchronized = useRef(void 0);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react-client.ts"],"sourcesContent":["'use client';\n\nimport { useEffect, useLayoutEffect, useMemo, useRef, useSyncExternalStore } from 'react';\nimport {\n type AssistantChatState,\n type AssistantClient,\n type AssistantContext,\n type AssistantModelContextUpdate,\n type AssistantPageContext,\n type CreateAssistantClientOptions,\n createAssistantClient,\n} from './client.js';\n\nexport interface UseNoodleAssistantOptions<\n TPageContext extends AssistantPageContext = AssistantContext,\n> extends CreateAssistantClientOptions<TPageContext> {\n /**\n * Browser-local identity for the authenticated principal/tenant.\n * Changing it discards the previous in-memory session and transcript; it is never sent to Noodle.\n */\n readonly principalKey: string;\n}\n\nexport type UseNoodleAssistantResult<TPageContext extends AssistantPageContext = AssistantContext> =\n AssistantChatState & {\n /** Canonical controller for turns, interactions, context, Apps requests, and aborts. */\n readonly client: AssistantClient<TPageContext>;\n };\n\ninterface AssistantReactStore {\n readonly getSnapshot: () => AssistantChatState;\n readonly subscribe: (listener: () => void) => () => void;\n}\n\nconst useCommittedLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;\n\n/**\n * Subscribe a customer-owned React renderer to the DOM-free assistant client.\n *\n * The hook owns only client lifetime and React state projection. Session exchange, transport,\n * retries, interaction continuations, and transcript construction remain client responsibilities.\n */\nexport function useNoodleAssistant<TPageContext extends AssistantPageContext = AssistantContext>(\n options: UseNoodleAssistantOptions<TPageContext>,\n): UseNoodleAssistantResult<TPageContext> {\n const optionsRef = useRef(options);\n const { fetch: injectedFetch, principalKey, sessionEndpoint } = options;\n\n const client = useMemo(() => {\n if (!principalKey.trim()) throw new Error('principalKey is required');\n return createAssistantClient<TPageContext>({\n sessionEndpoint,\n ...(injectedFetch ? { fetch: injectedFetch } : {}),\n clientContext: () => readProvider(optionsRef.current.clientContext),\n pageContext: () => readProvider(optionsRef.current.pageContext),\n });\n }, [injectedFetch, principalKey, sessionEndpoint]);\n const store = useMemo(() => createAssistantReactStore(client), [client]);\n const state = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);\n const synchronized = useRef<\n | {\n readonly client: AssistantClient<TPageContext>;\n readonly context: AssistantContext | undefined;\n readonly modelContext: AssistantModelContextUpdate | undefined;\n }\n | undefined\n >(undefined);\n\n useCommittedLayoutEffect(() => {\n optionsRef.current = options;\n const previous = synchronized.current;\n if (previous?.client === client) {\n if (previous.context !== options.context) {\n client.updateContext(options.context ?? {});\n }\n if (previous.modelContext !== options.modelContext) {\n client.updateModelContext(options.modelContext ?? {});\n }\n } else {\n if (options.context !== undefined) client.updateContext(options.context);\n if (options.modelContext !== undefined) client.updateModelContext(options.modelContext);\n }\n synchronized.current = {\n client,\n context: options.context,\n modelContext: options.modelContext,\n };\n }, [client, options]);\n\n useCommittedLayoutEffect(\n () => () => {\n client.abort();\n client.resetSession();\n },\n [client],\n );\n\n return useMemo(() => ({ client, ...state }), [client, state]);\n}\n\nfunction createAssistantReactStore<TPageContext extends AssistantPageContext>(\n client: AssistantClient<TPageContext>,\n): AssistantReactStore {\n let snapshot = client.getChatState();\n return {\n getSnapshot: () => snapshot,\n subscribe(listener) {\n let subscribing = true;\n const unsubscribe = client.subscribeChat((next) => {\n snapshot = next;\n if (!subscribing) listener();\n });\n subscribing = false;\n return unsubscribe;\n },\n };\n}\n\nfunction readProvider<T>(value: T | (() => T | undefined) | undefined): T | undefined {\n return typeof value === 'function' ? (value as () => T | undefined)() : value;\n}\n"],"mappings":";;;;;;;;AAEA,SAAS,WAAW,iBAAiB,SAAS,QAAQ,4BAA4B;AAgClF,IAAM,2BAA2B,OAAO,WAAW,cAAc,YAAY;AAQtE,SAAS,mBACd,SACwC;AACxC,QAAM,aAAa,OAAO,OAAO;AACjC,QAAM,EAAE,OAAO,eAAe,cAAc,gBAAgB,IAAI;AAEhE,QAAM,SAAS,QAAQ,MAAM;AAC3B,QAAI,CAAC,aAAa,KAAK,EAAG,OAAM,IAAI,MAAM,0BAA0B;AACpE,WAAO,sBAAoC;AAAA,MACzC;AAAA,MACA,GAAI,gBAAgB,EAAE,OAAO,cAAc,IAAI,CAAC;AAAA,MAChD,eAAe,MAAM,aAAa,WAAW,QAAQ,aAAa;AAAA,MAClE,aAAa,MAAM,aAAa,WAAW,QAAQ,WAAW;AAAA,IAChE,CAAC;AAAA,EACH,GAAG,CAAC,eAAe,cAAc,eAAe,CAAC;AACjD,QAAM,QAAQ,QAAQ,MAAM,0BAA0B,MAAM,GAAG,CAAC,MAAM,CAAC;AACvE,QAAM,QAAQ,qBAAqB,MAAM,WAAW,MAAM,aAAa,MAAM,WAAW;AACxF,QAAM,eAAe,OAOnB,MAAS;AAEX,2BAAyB,MAAM;AAC7B,eAAW,UAAU;AACrB,UAAM,WAAW,aAAa;AAC9B,QAAI,UAAU,WAAW,QAAQ;AAC/B,UAAI,SAAS,YAAY,QAAQ,SAAS;AACxC,eAAO,cAAc,QAAQ,WAAW,CAAC,CAAC;AAAA,MAC5C;AACA,UAAI,SAAS,iBAAiB,QAAQ,cAAc;AAClD,eAAO,mBAAmB,QAAQ,gBAAgB,CAAC,CAAC;AAAA,MACtD;AAAA,IACF,OAAO;AACL,UAAI,QAAQ,YAAY,OAAW,QAAO,cAAc,QAAQ,OAAO;AACvE,UAAI,QAAQ,iBAAiB,OAAW,QAAO,mBAAmB,QAAQ,YAAY;AAAA,IACxF;AACA,iBAAa,UAAU;AAAA,MACrB;AAAA,MACA,SAAS,QAAQ;AAAA,MACjB,cAAc,QAAQ;AAAA,IACxB;AAAA,EACF,GAAG,CAAC,QAAQ,OAAO,CAAC;AAEpB;AAAA,IACE,MAAM,MAAM;AACV,aAAO,MAAM;AACb,aAAO,aAAa;AAAA,IACtB;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AAEA,SAAO,QAAQ,OAAO,EAAE,QAAQ,GAAG,MAAM,IAAI,CAAC,QAAQ,KAAK,CAAC;AAC9D;AAEA,SAAS,0BACP,QACqB;AACrB,MAAI,WAAW,OAAO,aAAa;AACnC,SAAO;AAAA,IACL,aAAa,MAAM;AAAA,IACnB,UAAU,UAAU;AAClB,UAAI,cAAc;AAClB,YAAM,cAAc,OAAO,cAAc,CAAC,SAAS;AACjD,mBAAW;AACX,YAAI,CAAC,YAAa,UAAS;AAAA,MAC7B,CAAC;AACD,oBAAc;AACd,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAEA,SAAS,aAAgB,OAA6D;AACpF,SAAO,OAAO,UAAU,aAAc,MAA8B,IAAI;AAC1E;","names":[]}
1
+ {"version":3,"sources":["../../src/react-client.ts"],"sourcesContent":["'use client';\n\nimport { useEffect, useLayoutEffect, useMemo, useRef, useSyncExternalStore } from 'react';\nimport {\n type AssistantChatState,\n type AssistantClient,\n type AssistantContext,\n type AssistantModelContextUpdate,\n type AssistantPageContext,\n type CreateAssistantClientOptions,\n createAssistantClient,\n} from './client.js';\n\nexport type UseNoodleAssistantOptions<\n TPageContext extends AssistantPageContext = AssistantContext,\n> = CreateAssistantClientOptions<TPageContext> & {\n /**\n * Browser-local identity for the authenticated principal/tenant.\n * Changing it discards the previous in-memory session and transcript; it is never sent to Noodle.\n */\n readonly principalKey: string;\n};\n\nexport type UseNoodleAssistantResult<TPageContext extends AssistantPageContext = AssistantContext> =\n AssistantChatState & {\n /** Canonical controller for turns, interactions, context, Apps requests, and aborts. */\n readonly client: AssistantClient<TPageContext>;\n };\n\ninterface AssistantReactStore {\n readonly getSnapshot: () => AssistantChatState;\n readonly subscribe: (listener: () => void) => () => void;\n}\n\nconst useCommittedLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;\n\n/**\n * Subscribe a customer-owned React renderer to the DOM-free assistant client.\n *\n * The hook owns only client lifetime and React state projection. Session exchange, transport,\n * retries, interaction continuations, and transcript construction remain client responsibilities.\n */\nexport function useNoodleAssistant<TPageContext extends AssistantPageContext = AssistantContext>(\n options: UseNoodleAssistantOptions<TPageContext>,\n): UseNoodleAssistantResult<TPageContext> {\n const optionsRef = useRef(options);\n const { fetch: injectedFetch, principalKey, sessionEndpoint, embedId, serviceUrl } = options;\n\n const client = useMemo(() => {\n if (!principalKey.trim()) throw new Error('principalKey is required');\n // The two mounts are spelled out rather than spread from one object: the exclusive union is what\n // stops a caller passing both, and a spread would collapse it back into an ambiguous shape.\n const behavior = {\n ...(injectedFetch ? { fetch: injectedFetch } : {}),\n clientContext: () => readProvider(optionsRef.current.clientContext),\n pageContext: () => readProvider(optionsRef.current.pageContext),\n };\n return embedId !== undefined\n ? createAssistantClient<TPageContext>({\n embedId,\n ...(serviceUrl !== undefined ? { serviceUrl } : {}),\n ...behavior,\n })\n : createAssistantClient<TPageContext>({\n sessionEndpoint: sessionEndpoint ?? '',\n ...behavior,\n });\n }, [injectedFetch, principalKey, sessionEndpoint, embedId, serviceUrl]);\n const store = useMemo(() => createAssistantReactStore(client), [client]);\n const state = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);\n const synchronized = useRef<\n | {\n readonly client: AssistantClient<TPageContext>;\n readonly context: AssistantContext | undefined;\n readonly modelContext: AssistantModelContextUpdate | undefined;\n }\n | undefined\n >(undefined);\n\n useCommittedLayoutEffect(() => {\n optionsRef.current = options;\n const previous = synchronized.current;\n if (previous?.client === client) {\n if (previous.context !== options.context) {\n client.updateContext(options.context ?? {});\n }\n if (previous.modelContext !== options.modelContext) {\n client.updateModelContext(options.modelContext ?? {});\n }\n } else {\n if (options.context !== undefined) client.updateContext(options.context);\n if (options.modelContext !== undefined) client.updateModelContext(options.modelContext);\n }\n synchronized.current = {\n client,\n context: options.context,\n modelContext: options.modelContext,\n };\n }, [client, options]);\n\n useCommittedLayoutEffect(\n () => () => {\n client.abort();\n client.resetSession();\n },\n [client],\n );\n\n return useMemo(() => ({ client, ...state }), [client, state]);\n}\n\nfunction createAssistantReactStore<TPageContext extends AssistantPageContext>(\n client: AssistantClient<TPageContext>,\n): AssistantReactStore {\n let snapshot = client.getChatState();\n return {\n getSnapshot: () => snapshot,\n subscribe(listener) {\n let subscribing = true;\n const unsubscribe = client.subscribeChat((next) => {\n snapshot = next;\n if (!subscribing) listener();\n });\n subscribing = false;\n return unsubscribe;\n },\n };\n}\n\nfunction readProvider<T>(value: T | (() => T | undefined) | undefined): T | undefined {\n return typeof value === 'function' ? (value as () => T | undefined)() : value;\n}\n"],"mappings":";;;;;;;;AAEA,SAAS,WAAW,iBAAiB,SAAS,QAAQ,4BAA4B;AAgClF,IAAM,2BAA2B,OAAO,WAAW,cAAc,YAAY;AAQtE,SAAS,mBACd,SACwC;AACxC,QAAM,aAAa,OAAO,OAAO;AACjC,QAAM,EAAE,OAAO,eAAe,cAAc,iBAAiB,SAAS,WAAW,IAAI;AAErF,QAAM,SAAS,QAAQ,MAAM;AAC3B,QAAI,CAAC,aAAa,KAAK,EAAG,OAAM,IAAI,MAAM,0BAA0B;AAGpE,UAAM,WAAW;AAAA,MACf,GAAI,gBAAgB,EAAE,OAAO,cAAc,IAAI,CAAC;AAAA,MAChD,eAAe,MAAM,aAAa,WAAW,QAAQ,aAAa;AAAA,MAClE,aAAa,MAAM,aAAa,WAAW,QAAQ,WAAW;AAAA,IAChE;AACA,WAAO,YAAY,SACf,sBAAoC;AAAA,MAClC;AAAA,MACA,GAAI,eAAe,SAAY,EAAE,WAAW,IAAI,CAAC;AAAA,MACjD,GAAG;AAAA,IACL,CAAC,IACD,sBAAoC;AAAA,MAClC,iBAAiB,mBAAmB;AAAA,MACpC,GAAG;AAAA,IACL,CAAC;AAAA,EACP,GAAG,CAAC,eAAe,cAAc,iBAAiB,SAAS,UAAU,CAAC;AACtE,QAAM,QAAQ,QAAQ,MAAM,0BAA0B,MAAM,GAAG,CAAC,MAAM,CAAC;AACvE,QAAM,QAAQ,qBAAqB,MAAM,WAAW,MAAM,aAAa,MAAM,WAAW;AACxF,QAAM,eAAe,OAOnB,MAAS;AAEX,2BAAyB,MAAM;AAC7B,eAAW,UAAU;AACrB,UAAM,WAAW,aAAa;AAC9B,QAAI,UAAU,WAAW,QAAQ;AAC/B,UAAI,SAAS,YAAY,QAAQ,SAAS;AACxC,eAAO,cAAc,QAAQ,WAAW,CAAC,CAAC;AAAA,MAC5C;AACA,UAAI,SAAS,iBAAiB,QAAQ,cAAc;AAClD,eAAO,mBAAmB,QAAQ,gBAAgB,CAAC,CAAC;AAAA,MACtD;AAAA,IACF,OAAO;AACL,UAAI,QAAQ,YAAY,OAAW,QAAO,cAAc,QAAQ,OAAO;AACvE,UAAI,QAAQ,iBAAiB,OAAW,QAAO,mBAAmB,QAAQ,YAAY;AAAA,IACxF;AACA,iBAAa,UAAU;AAAA,MACrB;AAAA,MACA,SAAS,QAAQ;AAAA,MACjB,cAAc,QAAQ;AAAA,IACxB;AAAA,EACF,GAAG,CAAC,QAAQ,OAAO,CAAC;AAEpB;AAAA,IACE,MAAM,MAAM;AACV,aAAO,MAAM;AACb,aAAO,aAAa;AAAA,IACtB;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AAEA,SAAO,QAAQ,OAAO,EAAE,QAAQ,GAAG,MAAM,IAAI,CAAC,QAAQ,KAAK,CAAC;AAC9D;AAEA,SAAS,0BACP,QACqB;AACrB,MAAI,WAAW,OAAO,aAAa;AACnC,SAAO;AAAA,IACL,aAAa,MAAM;AAAA,IACnB,UAAU,UAAU;AAClB,UAAI,cAAc;AAClB,YAAM,cAAc,OAAO,cAAc,CAAC,SAAS;AACjD,mBAAW;AACX,YAAI,CAAC,YAAa,UAAS;AAAA,MAC7B,CAAC;AACD,oBAAc;AACd,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAEA,SAAS,aAAgB,OAA6D;AACpF,SAAO,OAAO,UAAU,aAAc,MAA8B,IAAI;AAC1E;","names":[]}
package/dist/react.cjs CHANGED
@@ -22191,7 +22191,7 @@ function mountAssistantApp(detail, actions, options = {}) {
22191
22191
  const current = options.getDetail?.() ?? detail;
22192
22192
  void bridge.sendSandboxResourceReady({
22193
22193
  html: current.html,
22194
- sandbox: "allow-scripts allow-forms",
22194
+ sandbox: "allow-scripts",
22195
22195
  ...current.resourceMeta ?? {}
22196
22196
  });
22197
22197
  };
@@ -26820,7 +26820,7 @@ var AssistantChatStateStore = class {
26820
26820
  this.#writeText(event.data.delta);
26821
26821
  return;
26822
26822
  case "tool_proposed":
26823
- this.#writeConfirmation(event.data);
26823
+ this.#writeStandaloneData(() => this.#writeConfirmation(event.data));
26824
26824
  return;
26825
26825
  case "interaction_proposed":
26826
26826
  this.#writeData("confirmation", event.data.id, {
@@ -26829,10 +26829,12 @@ var AssistantChatStateStore = class {
26829
26829
  });
26830
26830
  return;
26831
26831
  case "input_requested":
26832
- this.#writeData("input-request", event.data.id, {
26833
- ...event.data,
26834
- status: "pending"
26835
- });
26832
+ this.#writeStandaloneData(
26833
+ () => this.#writeData("input-request", event.data.id, {
26834
+ ...event.data,
26835
+ status: "pending"
26836
+ })
26837
+ );
26836
26838
  return;
26837
26839
  case "tool_completed":
26838
26840
  this.#writeData("tool-result", event.data.id, event.data);
@@ -26939,6 +26941,12 @@ var AssistantChatStateStore = class {
26939
26941
  #writeConfirmation(data) {
26940
26942
  this.#writeData("confirmation", data.id, { ...data, status: "pending" });
26941
26943
  }
26944
+ #writeStandaloneData(write) {
26945
+ const standalone = this.#operation === void 0;
26946
+ if (standalone) this.#startOperation();
26947
+ write();
26948
+ if (standalone) this.#closeOperation("ready");
26949
+ }
26942
26950
  #writeData(name21, id, data) {
26943
26951
  const operation = this.#operation;
26944
26952
  if (!operation || operation.closed) return;
@@ -27271,6 +27279,24 @@ function hasToJsonProperty(value) {
27271
27279
  return false;
27272
27280
  }
27273
27281
 
27282
+ // src/session-source.ts
27283
+ var NOODLE_CLOUD_URL = "https://cloud.noodleseed.dev";
27284
+ var PUBLIC_SESSION_PATH = "/v1/assistant/public-sessions";
27285
+ function resolveSessionSource(options) {
27286
+ const { sessionEndpoint, embedId, serviceUrl } = options;
27287
+ if (Boolean(embedId) === Boolean(sessionEndpoint)) {
27288
+ throw new Error("pass either embedId or sessionEndpoint, not both and not neither");
27289
+ }
27290
+ if (embedId) {
27291
+ const base = (serviceUrl ?? NOODLE_CLOUD_URL).replace(/\/+$/, "");
27292
+ return { kind: "public", url: `${base}${PUBLIC_SESSION_PATH}`, embedId };
27293
+ }
27294
+ return { kind: "exchange", url: sessionEndpoint };
27295
+ }
27296
+ function sessionSourceKey(options) {
27297
+ return `${options.embedId ?? ""}|${options.serviceUrl ?? ""}|${options.sessionEndpoint ?? ""}`;
27298
+ }
27299
+
27274
27300
  // src/transport.ts
27275
27301
  var AssistantTransportError = class extends Error {
27276
27302
  code = "invalid_response";
@@ -27398,6 +27424,19 @@ function isAbortError2(error51) {
27398
27424
  }
27399
27425
 
27400
27426
  // src/client.ts
27427
+ async function refusalCode(response) {
27428
+ try {
27429
+ const body = await response.clone().json();
27430
+ const code = body.code;
27431
+ return typeof code === "string" ? code : void 0;
27432
+ } catch {
27433
+ return void 0;
27434
+ }
27435
+ }
27436
+ var UNRETRYABLE_SERVICE_CODES = /* @__PURE__ */ new Set([
27437
+ "daily_turn_budget_exhausted",
27438
+ "daily_session_budget_exhausted"
27439
+ ]);
27401
27440
  var AssistantClientError = class extends Error {
27402
27441
  detail;
27403
27442
  constructor(detail, message, options) {
@@ -27407,7 +27446,7 @@ var AssistantClientError = class extends Error {
27407
27446
  }
27408
27447
  };
27409
27448
  var DefaultAssistantClient = class {
27410
- #sessionEndpoint;
27449
+ #source;
27411
27450
  #fetch;
27412
27451
  #listeners = /* @__PURE__ */ new Set();
27413
27452
  #chat = new AssistantChatStateStore();
@@ -27417,9 +27456,9 @@ var DefaultAssistantClient = class {
27417
27456
  #context;
27418
27457
  #modelContext;
27419
27458
  #active;
27459
+ #pendingAppInteractions = /* @__PURE__ */ new Map();
27420
27460
  constructor(options) {
27421
- if (!options.sessionEndpoint) throw new Error("sessionEndpoint is required");
27422
- this.#sessionEndpoint = options.sessionEndpoint;
27461
+ this.#source = resolveSessionSource(options);
27423
27462
  this.#fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
27424
27463
  this.#context = options.context ? { ...options.context } : void 0;
27425
27464
  this.#modelContext = options.modelContext ? copyAssistantModelContext(options.modelContext) : void 0;
@@ -27458,6 +27497,7 @@ var DefaultAssistantClient = class {
27458
27497
  }
27459
27498
  await this.#singleFlight(async (signal) => {
27460
27499
  await this.#runChatOperation(signal, async () => {
27500
+ const pendingApp = this.#pendingAppInteractions.get(id);
27461
27501
  const session = this.#session;
27462
27502
  if (!session) {
27463
27503
  throw clientError("confirmation_expired", "assistant session is not active", false);
@@ -27477,32 +27517,67 @@ var DefaultAssistantClient = class {
27477
27517
  ...resolution.action === "accept" && resolution.content !== void 0 ? { content: resolution.content } : {}
27478
27518
  } : { id };
27479
27519
  this.#emit({ event: "interaction_started", data: { id, action: resolution.action } });
27480
- const response = await this.#request(
27481
- endpoint,
27482
- {
27483
- method: "POST",
27484
- headers: authorizedHeaders(session.token, "text/event-stream"),
27485
- body: JSON.stringify(body),
27486
- signal
27487
- },
27488
- "confirmation_failed"
27489
- );
27490
- if (response.status === 401) {
27491
- this.#session = void 0;
27492
- this.#emit({ event: "session_expired", data: {} });
27493
- }
27494
- if (!response.ok) {
27495
- const serverCode = await readStableErrorCode(response);
27496
- const expired = response.status === 401 || response.status === 409;
27497
- throw clientError(
27498
- serverCode ?? (expired ? "confirmation_expired" : "confirmation_failed"),
27499
- `Assistant interaction failed (${response.status})`,
27500
- false,
27501
- response.status
27520
+ try {
27521
+ const response = await this.#request(
27522
+ endpoint,
27523
+ {
27524
+ method: "POST",
27525
+ headers: authorizedHeaders(session.token, "text/event-stream"),
27526
+ body: JSON.stringify(body),
27527
+ signal
27528
+ },
27529
+ "confirmation_failed"
27502
27530
  );
27531
+ if (response.status === 401) {
27532
+ this.#session = void 0;
27533
+ this.#emit({ event: "session_expired", data: {} });
27534
+ }
27535
+ if (!response.ok) {
27536
+ const serverCode = await readStableErrorCode(response);
27537
+ const expired = response.status === 401 || response.status === 409;
27538
+ throw clientError(
27539
+ serverCode ?? (expired ? "confirmation_expired" : "confirmation_failed"),
27540
+ `Assistant interaction failed (${response.status})`,
27541
+ false,
27542
+ response.status
27543
+ );
27544
+ }
27545
+ let completedResult;
27546
+ let nextInteractionId;
27547
+ await this.#consume(response, (event) => {
27548
+ if (event.event === "tool_completed" && event.data.id === id) {
27549
+ completedResult = event.data.result;
27550
+ }
27551
+ if ((event.event === "tool_proposed" || event.event === "input_requested") && event.data.id !== id) {
27552
+ nextInteractionId = event.data.id;
27553
+ }
27554
+ });
27555
+ this.#emit({ event: "interaction_completed", data: { id, action: resolution.action } });
27556
+ if (pendingApp) {
27557
+ this.#pendingAppInteractions.delete(id);
27558
+ if (nextInteractionId) {
27559
+ this.#pendingAppInteractions.set(nextInteractionId, pendingApp);
27560
+ } else if (resolution.action !== "accept") {
27561
+ pendingApp.resolve(appInteractionStopped(resolution.action));
27562
+ } else if (completedResult !== void 0) {
27563
+ pendingApp.resolve(appToolResult(completedResult));
27564
+ } else {
27565
+ pendingApp.reject(
27566
+ clientError(
27567
+ "invalid_response",
27568
+ "Assistant app interaction completed without a tool result",
27569
+ false
27570
+ )
27571
+ );
27572
+ }
27573
+ }
27574
+ } catch (error51) {
27575
+ if (pendingApp && this.#pendingAppInteractions.get(id) === pendingApp) {
27576
+ this.#pendingAppInteractions.delete(id);
27577
+ pendingApp.reject(error51);
27578
+ }
27579
+ throw error51;
27503
27580
  }
27504
- await this.#consume(response);
27505
- this.#emit({ event: "interaction_completed", data: { id, action: resolution.action } });
27506
27581
  });
27507
27582
  });
27508
27583
  }
@@ -27543,12 +27618,29 @@ var DefaultAssistantClient = class {
27543
27618
  response.status
27544
27619
  );
27545
27620
  }
27546
- return response.json();
27621
+ const value = await response.json();
27622
+ const interaction = parseAppInteraction(value);
27623
+ if (!interaction) return value;
27624
+ return new Promise((resolve2, reject) => {
27625
+ const id = interaction.data.id;
27626
+ const replaced = this.#pendingAppInteractions.get(id);
27627
+ if (replaced) {
27628
+ replaced.reject(
27629
+ clientError("invalid_response", "Assistant returned a duplicate app interaction", false)
27630
+ );
27631
+ }
27632
+ this.#pendingAppInteractions.set(id, { resolve: resolve2, reject });
27633
+ this.#emit(interaction);
27634
+ void this.#chat.flush();
27635
+ });
27547
27636
  }
27548
27637
  appSandboxUrl() {
27549
27638
  return this.#session?.endpoints.sandbox;
27550
27639
  }
27551
27640
  resetSession() {
27641
+ this.#rejectPendingAppInteractions(
27642
+ clientError("confirmation_expired", "assistant session is not active", false)
27643
+ );
27552
27644
  this.#session = void 0;
27553
27645
  this.#emit({ event: "session_reset", data: {} });
27554
27646
  }
@@ -27582,11 +27674,14 @@ var DefaultAssistantClient = class {
27582
27674
  return;
27583
27675
  }
27584
27676
  if (!response.ok) {
27677
+ const serviceCode = await refusalCode(response);
27585
27678
  throw clientError(
27586
27679
  "turn_failed",
27587
27680
  `Assistant turn failed (${response.status})`,
27588
27681
  false,
27589
- response.status
27682
+ response.status,
27683
+ void 0,
27684
+ serviceCode
27590
27685
  );
27591
27686
  }
27592
27687
  await this.#consume(response);
@@ -27625,23 +27720,25 @@ var DefaultAssistantClient = class {
27625
27720
  }
27626
27721
  }
27627
27722
  async #createSession(signal) {
27628
- const response = await this.#request(
27629
- this.#sessionEndpoint,
27630
- {
27631
- method: "POST",
27632
- headers: { Accept: "application/json", "Content-Type": "application/json" },
27633
- body: JSON.stringify(this.#context ? { context: this.#context } : {}),
27634
- credentials: "same-origin",
27635
- signal
27636
- },
27637
- "session_failed"
27638
- );
27723
+ const source = this.#source;
27724
+ const response = await this.#requestSession(source, {
27725
+ method: "POST",
27726
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
27727
+ body: JSON.stringify(
27728
+ source.kind === "public" ? { embedId: source.embedId } : this.#context ? { context: this.#context } : {}
27729
+ ),
27730
+ credentials: source.kind === "public" ? "omit" : "same-origin",
27731
+ signal
27732
+ });
27639
27733
  if (!response.ok) {
27734
+ const serviceCode = await refusalCode(response);
27640
27735
  throw clientError(
27641
27736
  "session_failed",
27642
27737
  `Assistant session failed (${response.status})`,
27643
- true,
27644
- response.status
27738
+ serviceCode === void 0 || !UNRETRYABLE_SERVICE_CODES.has(serviceCode),
27739
+ response.status,
27740
+ void 0,
27741
+ serviceCode
27645
27742
  );
27646
27743
  }
27647
27744
  let value;
@@ -27669,12 +27766,13 @@ var DefaultAssistantClient = class {
27669
27766
  });
27670
27767
  return session;
27671
27768
  }
27672
- async #consume(response) {
27769
+ async #consume(response, observe) {
27673
27770
  let streamError;
27674
27771
  try {
27675
27772
  await consumeAssistantEvents(response, (event) => {
27676
27773
  const clientEvent = toAssistantClientEvent(event);
27677
27774
  this.#emit(clientEvent);
27775
+ observe?.(clientEvent);
27678
27776
  if (clientEvent.event !== "error" || typeof clientEvent.data.code !== "string") return;
27679
27777
  streamError = {
27680
27778
  code: clientEvent.data.code,
@@ -27697,6 +27795,34 @@ var DefaultAssistantClient = class {
27697
27795
  );
27698
27796
  }
27699
27797
  }
27798
+ /**
27799
+ * The mint, with the one failure a public page hits that an in-app embed cannot.
27800
+ *
27801
+ * A page that loads the embed script and then blocks `connect-src` fails here as a bare network
27802
+ * rejection — no status, no body, nothing to read. "Assistant request failed" sends a developer
27803
+ * hunting through their own code; naming the directive and the origin ends the search. The message
27804
+ * covers a plain outage too, because from inside the browser the two are indistinguishable.
27805
+ */
27806
+ async #requestSession(source, init) {
27807
+ try {
27808
+ return await this.#fetch(source.url, init);
27809
+ } catch (error51) {
27810
+ if (init.signal?.aborted) throw error51;
27811
+ if (source.kind !== "public") {
27812
+ throw clientError("session_failed", "Assistant request failed", true, void 0, {
27813
+ cause: error51
27814
+ });
27815
+ }
27816
+ throw clientError(
27817
+ "session_failed",
27818
+ `Assistant could not reach ${new URL(source.url).origin}. If the page sets a Content-Security-Policy, allow that origin in connect-src (and in script-src and frame-src).`,
27819
+ true,
27820
+ void 0,
27821
+ { cause: error51 },
27822
+ "blocked_by_page"
27823
+ );
27824
+ }
27825
+ }
27700
27826
  async #request(input, init, failureCode) {
27701
27827
  try {
27702
27828
  return await this.#fetch(input, init);
@@ -27743,6 +27869,10 @@ var DefaultAssistantClient = class {
27743
27869
  }
27744
27870
  }
27745
27871
  }
27872
+ #rejectPendingAppInteractions(error51) {
27873
+ for (const pending of this.#pendingAppInteractions.values()) pending.reject(error51);
27874
+ this.#pendingAppInteractions.clear();
27875
+ }
27746
27876
  };
27747
27877
  function createAssistantClient(options) {
27748
27878
  return new DefaultAssistantClient(options);
@@ -27754,6 +27884,30 @@ function authorizedHeaders(token, accept) {
27754
27884
  Accept: accept
27755
27885
  };
27756
27886
  }
27887
+ function parseAppInteraction(value) {
27888
+ if (!isRecord4(value) || !isRecord4(value.interaction)) return void 0;
27889
+ const event = value.interaction.event;
27890
+ const data = value.interaction.data;
27891
+ if (event !== "tool_proposed" && event !== "input_requested" || !isRecord4(data)) {
27892
+ return void 0;
27893
+ }
27894
+ const parsed = toAssistantClientEvent({ event, data });
27895
+ return parsed.event === "tool_proposed" || parsed.event === "input_requested" ? parsed : void 0;
27896
+ }
27897
+ function appToolResult(result) {
27898
+ const text3 = typeof result === "string" ? result : JSON.stringify(result);
27899
+ return result !== null && typeof result === "object" && !Array.isArray(result) ? {
27900
+ content: [{ type: "text", text: text3 }],
27901
+ structuredContent: result,
27902
+ isError: false
27903
+ } : { content: [{ type: "text", text: text3 }], isError: false };
27904
+ }
27905
+ function appInteractionStopped(action) {
27906
+ return {
27907
+ content: [{ type: "text", text: `interaction_${action}` }],
27908
+ isError: true
27909
+ };
27910
+ }
27757
27911
  function parseSession(value) {
27758
27912
  if (!isRecord4(value) || !isRecord4(value.endpoints)) {
27759
27913
  throw clientError("session_failed", "Assistant session response is invalid", true);
@@ -27790,9 +27944,14 @@ async function readStableErrorCode(response) {
27790
27944
  if (!isRecord4(value) || typeof value.code !== "string") return void 0;
27791
27945
  return /^[A-Za-z0-9_.-]{1,64}$/.test(value.code) ? value.code : void 0;
27792
27946
  }
27793
- function clientError(code, message, retryable, status, options) {
27947
+ function clientError(code, message, retryable, status, options, serviceCode) {
27794
27948
  return new AssistantClientError(
27795
- { code, ...status === void 0 ? {} : { status }, retryable },
27949
+ {
27950
+ code,
27951
+ ...status === void 0 ? {} : { status },
27952
+ retryable,
27953
+ ...serviceCode === void 0 ? {} : { serviceCode }
27954
+ },
27796
27955
  message,
27797
27956
  options
27798
27957
  );
@@ -30969,7 +31128,6 @@ var AssistantElementEventController = class {
30969
31128
  return;
30970
31129
  }
30971
31130
  if (name21 === "tool_proposed" || name21 === "interaction_proposed") {
30972
- if (!this.#streamKind) return;
30973
31131
  this.#appendToolProposal(
30974
31132
  String(data.id ?? ""),
30975
31133
  String(data.tool ?? "action"),
@@ -30981,7 +31139,6 @@ var AssistantElementEventController = class {
30981
31139
  return;
30982
31140
  }
30983
31141
  if (name21 === "input_requested" && isRecord5(data.requestedSchema)) {
30984
- if (!this.#streamKind) return;
30985
31142
  this.#appendInputRequest(
30986
31143
  String(data.id ?? ""),
30987
31144
  String(data.message ?? "More information is required"),
@@ -31885,6 +32042,10 @@ var NoodleAssistantElement = class extends HTMLElementBase {
31885
32042
  #appearanceStyleSnapshot = /* @__PURE__ */ new Map();
31886
32043
  #scroll = new AssistantElementScrollController();
31887
32044
  sessionEndpoint = "";
32045
+ /** Public mount: the non-secret embed id `noodle deploy` printed. Mutually exclusive with the above. */
32046
+ embedId = "";
32047
+ /** Only for a dev or self-hosted service; the published snippet needs no origin. */
32048
+ serviceUrl = "";
31888
32049
  get appearance() {
31889
32050
  return this.#hostAppearance;
31890
32051
  }
@@ -31939,6 +32100,8 @@ var NoodleAssistantElement = class extends HTMLElementBase {
31939
32100
  connectedCallback() {
31940
32101
  if (!this.shadowRoot) this.attachShadow({ mode: "open" });
31941
32102
  if (!this.sessionEndpoint) this.sessionEndpoint = this.getAttribute("session-endpoint") ?? "";
32103
+ if (!this.embedId) this.embedId = this.getAttribute("embed-id") ?? "";
32104
+ if (!this.serviceUrl) this.serviceUrl = this.getAttribute("service-url") ?? "";
31942
32105
  this.#media = globalThis.matchMedia?.("(prefers-color-scheme: dark)");
31943
32106
  this.#media?.addEventListener("change", this.#handleSystemTheme);
31944
32107
  document.addEventListener("keydown", this.#handleDocumentKeydown);
@@ -32110,26 +32273,31 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32110
32273
  }
32111
32274
  };
32112
32275
  #ensureClient() {
32113
- if (this.#client && this.#clientEndpoint === this.sessionEndpoint) return this.#client;
32276
+ const key = sessionSourceKey(this);
32277
+ if (this.#client && this.#clientEndpoint === key) return this.#client;
32114
32278
  this.#client?.abort();
32115
32279
  this.#unsubscribeClient?.();
32116
32280
  this.#sessionBootstrap = void 0;
32117
- const client = createAssistantClient({
32118
- sessionEndpoint: this.sessionEndpoint,
32281
+ const behavior = {
32119
32282
  ...this.#context ? { context: this.#context } : {},
32120
32283
  clientContext: browserClientContext,
32121
32284
  ...this.#pageContext ? { pageContext: this.#pageContext } : {},
32122
32285
  ...this.#modelContext ? { modelContext: this.#modelContext } : {}
32123
- });
32286
+ };
32287
+ const client = this.embedId ? createAssistantClient({
32288
+ embedId: this.embedId,
32289
+ ...this.serviceUrl ? { serviceUrl: this.serviceUrl } : {},
32290
+ ...behavior
32291
+ }) : createAssistantClient({ sessionEndpoint: this.sessionEndpoint, ...behavior });
32124
32292
  this.#client = client;
32125
- this.#clientEndpoint = this.sessionEndpoint;
32293
+ this.#clientEndpoint = key;
32126
32294
  this.#unsubscribeClient = client.subscribe((event) => {
32127
32295
  if (this.#client === client) this.#handleClientEvent(event);
32128
32296
  });
32129
32297
  return client;
32130
32298
  }
32131
32299
  #primeSession() {
32132
- if (!this.sessionEndpoint) return;
32300
+ if (!this.sessionEndpoint && !this.embedId) return;
32133
32301
  const client = this.#ensureClient();
32134
32302
  const generation = this.#conversationGeneration;
32135
32303
  this.#setSessionState("loading");
@@ -32253,23 +32421,30 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32253
32421
  }
32254
32422
  const detail = error51 instanceof AssistantClientError ? error51.detail : { code: fallbackCode, retryable: false };
32255
32423
  this.#appendRecoveryError(
32256
- detail.code === "session_expired" ? this.#appearance.labels.sessionExpired : this.#appearance.labels.unavailable
32424
+ detail.code === "session_expired" ? this.#appearance.labels.sessionExpired : this.#appearance.labels.unavailable,
32425
+ // A spent or switched-off daily budget offers no retry. Reconnecting cannot succeed, and every
32426
+ // open tab trying is exactly the load the cap was set to refuse — so the button goes, not just
32427
+ // the alarming words. A spent *session* keeps it: reconnect mints a fresh one, which is the fix.
32428
+ detail.retryable !== false || detail.serviceCode === void 0
32257
32429
  );
32258
32430
  this.#dispatchError(detail);
32259
32431
  }
32260
- #appendRecoveryError(message) {
32432
+ #appendRecoveryError(message, offerRetry = true) {
32261
32433
  if (!this.#messages) return;
32262
32434
  this.#messages.querySelector(".conversation-error")?.remove();
32263
32435
  const error51 = document.createElement("section");
32264
32436
  error51.className = "conversation-error";
32265
- error51.setAttribute("role", "alert");
32437
+ error51.setAttribute("role", offerRetry ? "alert" : "status");
32266
32438
  const text3 = document.createElement("p");
32267
32439
  text3.textContent = message;
32268
- const retry = document.createElement("button");
32269
- retry.type = "button";
32270
- retry.textContent = this.#appearance.labels.reconnect;
32271
- retry.addEventListener("click", () => this.reconnect());
32272
- error51.append(text3, retry);
32440
+ error51.append(text3);
32441
+ if (offerRetry) {
32442
+ const retry = document.createElement("button");
32443
+ retry.type = "button";
32444
+ retry.textContent = this.#appearance.labels.reconnect;
32445
+ retry.addEventListener("click", () => this.reconnect());
32446
+ error51.append(retry);
32447
+ }
32273
32448
  this.#messages.append(error51);
32274
32449
  this.#revealLatest();
32275
32450
  }
@@ -32547,7 +32722,9 @@ var NoodleAssistantWithRef = (0, import_react2.forwardRef)(
32547
32722
  element.addEventListener("assistant-event", assistantEvent);
32548
32723
  element.addEventListener("assistant-appearance-warning", appearanceWarning);
32549
32724
  registerNoodleAssistant();
32550
- element.sessionEndpoint = props.sessionEndpoint;
32725
+ element.sessionEndpoint = props.sessionEndpoint ?? "";
32726
+ element.embedId = props.embedId ?? "";
32727
+ element.serviceUrl = props.serviceUrl ?? "";
32551
32728
  element.appearance = props.appearance;
32552
32729
  element.toggleAttribute("open", props.open === true);
32553
32730
  if (props.context) element.updateContext(props.context);
@@ -32572,12 +32749,16 @@ var NoodleAssistantWithRef = (0, import_react2.forwardRef)(
32572
32749
  props.onSessionExpired,
32573
32750
  props.open,
32574
32751
  props.pageContext,
32575
- props.sessionEndpoint
32752
+ props.sessionEndpoint,
32753
+ props.embedId,
32754
+ props.serviceUrl
32576
32755
  ]);
32577
32756
  return (0, import_react2.createElement)("noodle-assistant", {
32578
32757
  ref,
32579
32758
  class: props.className,
32580
- "session-endpoint": props.sessionEndpoint,
32759
+ ...props.sessionEndpoint ? { "session-endpoint": props.sessionEndpoint } : {},
32760
+ ...props.embedId ? { "embed-id": props.embedId } : {},
32761
+ ...props.serviceUrl ? { "service-url": props.serviceUrl } : {},
32581
32762
  ...props.theme ? { theme: props.theme } : {}
32582
32763
  });
32583
32764
  }