@noodleseed/assistant 1.14.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-RITHSCTD.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
@@ -27279,6 +27279,24 @@ function hasToJsonProperty(value) {
27279
27279
  return false;
27280
27280
  }
27281
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
+
27282
27300
  // src/transport.ts
27283
27301
  var AssistantTransportError = class extends Error {
27284
27302
  code = "invalid_response";
@@ -27406,6 +27424,19 @@ function isAbortError2(error51) {
27406
27424
  }
27407
27425
 
27408
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
+ ]);
27409
27440
  var AssistantClientError = class extends Error {
27410
27441
  detail;
27411
27442
  constructor(detail, message, options) {
@@ -27415,7 +27446,7 @@ var AssistantClientError = class extends Error {
27415
27446
  }
27416
27447
  };
27417
27448
  var DefaultAssistantClient = class {
27418
- #sessionEndpoint;
27449
+ #source;
27419
27450
  #fetch;
27420
27451
  #listeners = /* @__PURE__ */ new Set();
27421
27452
  #chat = new AssistantChatStateStore();
@@ -27427,8 +27458,7 @@ var DefaultAssistantClient = class {
27427
27458
  #active;
27428
27459
  #pendingAppInteractions = /* @__PURE__ */ new Map();
27429
27460
  constructor(options) {
27430
- if (!options.sessionEndpoint) throw new Error("sessionEndpoint is required");
27431
- this.#sessionEndpoint = options.sessionEndpoint;
27461
+ this.#source = resolveSessionSource(options);
27432
27462
  this.#fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
27433
27463
  this.#context = options.context ? { ...options.context } : void 0;
27434
27464
  this.#modelContext = options.modelContext ? copyAssistantModelContext(options.modelContext) : void 0;
@@ -27644,11 +27674,14 @@ var DefaultAssistantClient = class {
27644
27674
  return;
27645
27675
  }
27646
27676
  if (!response.ok) {
27677
+ const serviceCode = await refusalCode(response);
27647
27678
  throw clientError(
27648
27679
  "turn_failed",
27649
27680
  `Assistant turn failed (${response.status})`,
27650
27681
  false,
27651
- response.status
27682
+ response.status,
27683
+ void 0,
27684
+ serviceCode
27652
27685
  );
27653
27686
  }
27654
27687
  await this.#consume(response);
@@ -27687,23 +27720,25 @@ var DefaultAssistantClient = class {
27687
27720
  }
27688
27721
  }
27689
27722
  async #createSession(signal) {
27690
- const response = await this.#request(
27691
- this.#sessionEndpoint,
27692
- {
27693
- method: "POST",
27694
- headers: { Accept: "application/json", "Content-Type": "application/json" },
27695
- body: JSON.stringify(this.#context ? { context: this.#context } : {}),
27696
- credentials: "same-origin",
27697
- signal
27698
- },
27699
- "session_failed"
27700
- );
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
+ });
27701
27733
  if (!response.ok) {
27734
+ const serviceCode = await refusalCode(response);
27702
27735
  throw clientError(
27703
27736
  "session_failed",
27704
27737
  `Assistant session failed (${response.status})`,
27705
- true,
27706
- response.status
27738
+ serviceCode === void 0 || !UNRETRYABLE_SERVICE_CODES.has(serviceCode),
27739
+ response.status,
27740
+ void 0,
27741
+ serviceCode
27707
27742
  );
27708
27743
  }
27709
27744
  let value;
@@ -27760,6 +27795,34 @@ var DefaultAssistantClient = class {
27760
27795
  );
27761
27796
  }
27762
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
+ }
27763
27826
  async #request(input, init, failureCode) {
27764
27827
  try {
27765
27828
  return await this.#fetch(input, init);
@@ -27881,9 +27944,14 @@ async function readStableErrorCode(response) {
27881
27944
  if (!isRecord4(value) || typeof value.code !== "string") return void 0;
27882
27945
  return /^[A-Za-z0-9_.-]{1,64}$/.test(value.code) ? value.code : void 0;
27883
27946
  }
27884
- function clientError(code, message, retryable, status, options) {
27947
+ function clientError(code, message, retryable, status, options, serviceCode) {
27885
27948
  return new AssistantClientError(
27886
- { code, ...status === void 0 ? {} : { status }, retryable },
27949
+ {
27950
+ code,
27951
+ ...status === void 0 ? {} : { status },
27952
+ retryable,
27953
+ ...serviceCode === void 0 ? {} : { serviceCode }
27954
+ },
27887
27955
  message,
27888
27956
  options
27889
27957
  );
@@ -31974,6 +32042,10 @@ var NoodleAssistantElement = class extends HTMLElementBase {
31974
32042
  #appearanceStyleSnapshot = /* @__PURE__ */ new Map();
31975
32043
  #scroll = new AssistantElementScrollController();
31976
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 = "";
31977
32049
  get appearance() {
31978
32050
  return this.#hostAppearance;
31979
32051
  }
@@ -32028,6 +32100,8 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32028
32100
  connectedCallback() {
32029
32101
  if (!this.shadowRoot) this.attachShadow({ mode: "open" });
32030
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") ?? "";
32031
32105
  this.#media = globalThis.matchMedia?.("(prefers-color-scheme: dark)");
32032
32106
  this.#media?.addEventListener("change", this.#handleSystemTheme);
32033
32107
  document.addEventListener("keydown", this.#handleDocumentKeydown);
@@ -32199,26 +32273,31 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32199
32273
  }
32200
32274
  };
32201
32275
  #ensureClient() {
32202
- 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;
32203
32278
  this.#client?.abort();
32204
32279
  this.#unsubscribeClient?.();
32205
32280
  this.#sessionBootstrap = void 0;
32206
- const client = createAssistantClient({
32207
- sessionEndpoint: this.sessionEndpoint,
32281
+ const behavior = {
32208
32282
  ...this.#context ? { context: this.#context } : {},
32209
32283
  clientContext: browserClientContext,
32210
32284
  ...this.#pageContext ? { pageContext: this.#pageContext } : {},
32211
32285
  ...this.#modelContext ? { modelContext: this.#modelContext } : {}
32212
- });
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 });
32213
32292
  this.#client = client;
32214
- this.#clientEndpoint = this.sessionEndpoint;
32293
+ this.#clientEndpoint = key;
32215
32294
  this.#unsubscribeClient = client.subscribe((event) => {
32216
32295
  if (this.#client === client) this.#handleClientEvent(event);
32217
32296
  });
32218
32297
  return client;
32219
32298
  }
32220
32299
  #primeSession() {
32221
- if (!this.sessionEndpoint) return;
32300
+ if (!this.sessionEndpoint && !this.embedId) return;
32222
32301
  const client = this.#ensureClient();
32223
32302
  const generation = this.#conversationGeneration;
32224
32303
  this.#setSessionState("loading");
@@ -32342,23 +32421,30 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32342
32421
  }
32343
32422
  const detail = error51 instanceof AssistantClientError ? error51.detail : { code: fallbackCode, retryable: false };
32344
32423
  this.#appendRecoveryError(
32345
- 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
32346
32429
  );
32347
32430
  this.#dispatchError(detail);
32348
32431
  }
32349
- #appendRecoveryError(message) {
32432
+ #appendRecoveryError(message, offerRetry = true) {
32350
32433
  if (!this.#messages) return;
32351
32434
  this.#messages.querySelector(".conversation-error")?.remove();
32352
32435
  const error51 = document.createElement("section");
32353
32436
  error51.className = "conversation-error";
32354
- error51.setAttribute("role", "alert");
32437
+ error51.setAttribute("role", offerRetry ? "alert" : "status");
32355
32438
  const text3 = document.createElement("p");
32356
32439
  text3.textContent = message;
32357
- const retry = document.createElement("button");
32358
- retry.type = "button";
32359
- retry.textContent = this.#appearance.labels.reconnect;
32360
- retry.addEventListener("click", () => this.reconnect());
32361
- 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
+ }
32362
32448
  this.#messages.append(error51);
32363
32449
  this.#revealLatest();
32364
32450
  }
@@ -32636,7 +32722,9 @@ var NoodleAssistantWithRef = (0, import_react2.forwardRef)(
32636
32722
  element.addEventListener("assistant-event", assistantEvent);
32637
32723
  element.addEventListener("assistant-appearance-warning", appearanceWarning);
32638
32724
  registerNoodleAssistant();
32639
- element.sessionEndpoint = props.sessionEndpoint;
32725
+ element.sessionEndpoint = props.sessionEndpoint ?? "";
32726
+ element.embedId = props.embedId ?? "";
32727
+ element.serviceUrl = props.serviceUrl ?? "";
32640
32728
  element.appearance = props.appearance;
32641
32729
  element.toggleAttribute("open", props.open === true);
32642
32730
  if (props.context) element.updateContext(props.context);
@@ -32661,12 +32749,16 @@ var NoodleAssistantWithRef = (0, import_react2.forwardRef)(
32661
32749
  props.onSessionExpired,
32662
32750
  props.open,
32663
32751
  props.pageContext,
32664
- props.sessionEndpoint
32752
+ props.sessionEndpoint,
32753
+ props.embedId,
32754
+ props.serviceUrl
32665
32755
  ]);
32666
32756
  return (0, import_react2.createElement)("noodle-assistant", {
32667
32757
  ref,
32668
32758
  class: props.className,
32669
- "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 } : {},
32670
32762
  ...props.theme ? { theme: props.theme } : {}
32671
32763
  });
32672
32764
  }