@noodleseed/assistant 1.14.0 → 1.16.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-401BAJqk.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-Ii_xTV09.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-NPV64PUX.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
@@ -27095,6 +27095,11 @@ function toAssistantClientEvent(event) {
27095
27095
  return event;
27096
27096
  }
27097
27097
  break;
27098
+ case "auth_requested":
27099
+ if (hasString(value, "id") && hasString(value, "tool") && hasString(value, "continuation") && hasString(value, "expiresAt") && hasOptionalTurnId(value)) {
27100
+ return event;
27101
+ }
27102
+ break;
27098
27103
  case "interaction_resolved":
27099
27104
  if (hasString(value, "id") && (value.action === void 0 || isInteractionAction(value.action)) && hasOptionalTurnId(value)) {
27100
27105
  return event;
@@ -27279,6 +27284,24 @@ function hasToJsonProperty(value) {
27279
27284
  return false;
27280
27285
  }
27281
27286
 
27287
+ // src/session-source.ts
27288
+ var NOODLE_CLOUD_URL = "https://cloud.noodleseed.dev";
27289
+ var PUBLIC_SESSION_PATH = "/v1/assistant/public-sessions";
27290
+ function resolveSessionSource(options) {
27291
+ const { sessionEndpoint, embedId, serviceUrl } = options;
27292
+ if (Boolean(embedId) === Boolean(sessionEndpoint)) {
27293
+ throw new Error("pass either embedId or sessionEndpoint, not both and not neither");
27294
+ }
27295
+ if (embedId) {
27296
+ const base = (serviceUrl ?? NOODLE_CLOUD_URL).replace(/\/+$/, "");
27297
+ return { kind: "public", url: `${base}${PUBLIC_SESSION_PATH}`, embedId };
27298
+ }
27299
+ return { kind: "exchange", url: sessionEndpoint };
27300
+ }
27301
+ function sessionSourceKey(options) {
27302
+ return `${options.embedId ?? ""}|${options.serviceUrl ?? ""}|${options.sessionEndpoint ?? ""}`;
27303
+ }
27304
+
27282
27305
  // src/transport.ts
27283
27306
  var AssistantTransportError = class extends Error {
27284
27307
  code = "invalid_response";
@@ -27406,6 +27429,19 @@ function isAbortError2(error51) {
27406
27429
  }
27407
27430
 
27408
27431
  // src/client.ts
27432
+ async function refusalCode(response) {
27433
+ try {
27434
+ const body = await response.clone().json();
27435
+ const code = body.code;
27436
+ return typeof code === "string" ? code : void 0;
27437
+ } catch {
27438
+ return void 0;
27439
+ }
27440
+ }
27441
+ var UNRETRYABLE_SERVICE_CODES = /* @__PURE__ */ new Set([
27442
+ "daily_turn_budget_exhausted",
27443
+ "daily_session_budget_exhausted"
27444
+ ]);
27409
27445
  var AssistantClientError = class extends Error {
27410
27446
  detail;
27411
27447
  constructor(detail, message, options) {
@@ -27415,7 +27451,7 @@ var AssistantClientError = class extends Error {
27415
27451
  }
27416
27452
  };
27417
27453
  var DefaultAssistantClient = class {
27418
- #sessionEndpoint;
27454
+ #source;
27419
27455
  #fetch;
27420
27456
  #listeners = /* @__PURE__ */ new Set();
27421
27457
  #chat = new AssistantChatStateStore();
@@ -27427,8 +27463,7 @@ var DefaultAssistantClient = class {
27427
27463
  #active;
27428
27464
  #pendingAppInteractions = /* @__PURE__ */ new Map();
27429
27465
  constructor(options) {
27430
- if (!options.sessionEndpoint) throw new Error("sessionEndpoint is required");
27431
- this.#sessionEndpoint = options.sessionEndpoint;
27466
+ this.#source = resolveSessionSource(options);
27432
27467
  this.#fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
27433
27468
  this.#context = options.context ? { ...options.context } : void 0;
27434
27469
  this.#modelContext = options.modelContext ? copyAssistantModelContext(options.modelContext) : void 0;
@@ -27644,11 +27679,14 @@ var DefaultAssistantClient = class {
27644
27679
  return;
27645
27680
  }
27646
27681
  if (!response.ok) {
27682
+ const serviceCode = await refusalCode(response);
27647
27683
  throw clientError(
27648
27684
  "turn_failed",
27649
27685
  `Assistant turn failed (${response.status})`,
27650
27686
  false,
27651
- response.status
27687
+ response.status,
27688
+ void 0,
27689
+ serviceCode
27652
27690
  );
27653
27691
  }
27654
27692
  await this.#consume(response);
@@ -27687,23 +27725,25 @@ var DefaultAssistantClient = class {
27687
27725
  }
27688
27726
  }
27689
27727
  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
- );
27728
+ const source = this.#source;
27729
+ const response = await this.#requestSession(source, {
27730
+ method: "POST",
27731
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
27732
+ body: JSON.stringify(
27733
+ source.kind === "public" ? { embedId: source.embedId } : this.#context ? { context: this.#context } : {}
27734
+ ),
27735
+ credentials: source.kind === "public" ? "omit" : "same-origin",
27736
+ signal
27737
+ });
27701
27738
  if (!response.ok) {
27739
+ const serviceCode = await refusalCode(response);
27702
27740
  throw clientError(
27703
27741
  "session_failed",
27704
27742
  `Assistant session failed (${response.status})`,
27705
- true,
27706
- response.status
27743
+ serviceCode === void 0 || !UNRETRYABLE_SERVICE_CODES.has(serviceCode),
27744
+ response.status,
27745
+ void 0,
27746
+ serviceCode
27707
27747
  );
27708
27748
  }
27709
27749
  let value;
@@ -27760,6 +27800,34 @@ var DefaultAssistantClient = class {
27760
27800
  );
27761
27801
  }
27762
27802
  }
27803
+ /**
27804
+ * The mint, with the one failure a public page hits that an in-app embed cannot.
27805
+ *
27806
+ * A page that loads the embed script and then blocks `connect-src` fails here as a bare network
27807
+ * rejection — no status, no body, nothing to read. "Assistant request failed" sends a developer
27808
+ * hunting through their own code; naming the directive and the origin ends the search. The message
27809
+ * covers a plain outage too, because from inside the browser the two are indistinguishable.
27810
+ */
27811
+ async #requestSession(source, init) {
27812
+ try {
27813
+ return await this.#fetch(source.url, init);
27814
+ } catch (error51) {
27815
+ if (init.signal?.aborted) throw error51;
27816
+ if (source.kind !== "public") {
27817
+ throw clientError("session_failed", "Assistant request failed", true, void 0, {
27818
+ cause: error51
27819
+ });
27820
+ }
27821
+ throw clientError(
27822
+ "session_failed",
27823
+ `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).`,
27824
+ true,
27825
+ void 0,
27826
+ { cause: error51 },
27827
+ "blocked_by_page"
27828
+ );
27829
+ }
27830
+ }
27763
27831
  async #request(input, init, failureCode) {
27764
27832
  try {
27765
27833
  return await this.#fetch(input, init);
@@ -27881,9 +27949,14 @@ async function readStableErrorCode(response) {
27881
27949
  if (!isRecord4(value) || typeof value.code !== "string") return void 0;
27882
27950
  return /^[A-Za-z0-9_.-]{1,64}$/.test(value.code) ? value.code : void 0;
27883
27951
  }
27884
- function clientError(code, message, retryable, status, options) {
27952
+ function clientError(code, message, retryable, status, options, serviceCode) {
27885
27953
  return new AssistantClientError(
27886
- { code, ...status === void 0 ? {} : { status }, retryable },
27954
+ {
27955
+ code,
27956
+ ...status === void 0 ? {} : { status },
27957
+ retryable,
27958
+ ...serviceCode === void 0 ? {} : { serviceCode }
27959
+ },
27887
27960
  message,
27888
27961
  options
27889
27962
  );
@@ -30976,6 +31049,28 @@ function renderMarkdown(text3) {
30976
31049
  });
30977
31050
  }
30978
31051
 
31052
+ // src/sign-in-card.ts
31053
+ function createSignInCard(options) {
31054
+ const card = document.createElement("div");
31055
+ card.className = "noodle-assistant-card noodle-assistant-sign-in";
31056
+ card.dataset.tool = options.tool;
31057
+ card.setAttribute("role", "status");
31058
+ const heading = document.createElement("p");
31059
+ heading.className = "noodle-assistant-card-heading";
31060
+ heading.textContent = options.labels.heading;
31061
+ card.append(heading);
31062
+ const action = document.createElement("button");
31063
+ action.type = "button";
31064
+ action.className = "noodle-assistant-card-action";
31065
+ action.textContent = options.labels.action;
31066
+ action.addEventListener("click", () => {
31067
+ action.disabled = true;
31068
+ options.onSignIn();
31069
+ });
31070
+ card.append(action);
31071
+ return card;
31072
+ }
31073
+
30979
31074
  // src/element-event-controller.ts
30980
31075
  var AssistantElementEventController = class {
30981
31076
  #host;
@@ -31070,6 +31165,15 @@ var AssistantElementEventController = class {
31070
31165
  );
31071
31166
  return;
31072
31167
  }
31168
+ if (name21 === "auth_requested" && typeof data.continuation === "string") {
31169
+ this.#appendSignInRequest(
31170
+ String(data.id ?? ""),
31171
+ String(data.tool ?? "this"),
31172
+ data.continuation,
31173
+ String(data.expiresAt ?? "")
31174
+ );
31175
+ return;
31176
+ }
31073
31177
  if (name21 === "input_requested" && isRecord5(data.requestedSchema)) {
31074
31178
  this.#appendInputRequest(
31075
31179
  String(data.id ?? ""),
@@ -31155,6 +31259,40 @@ var AssistantElementEventController = class {
31155
31259
  })
31156
31260
  );
31157
31261
  }
31262
+ /**
31263
+ * The host application owns the login. This renders the prompt and raises
31264
+ * `assistant-sign-in-requested`; the page signs the visitor in however it already does, then its
31265
+ * backend spends the continuation. Nothing here talks to an identity provider.
31266
+ */
31267
+ #appendSignInRequest(id, tool, continuation, expiresAt) {
31268
+ const messages = this.#host.messages();
31269
+ if (!messages) return;
31270
+ this.#proposalCards.get(id)?.remove();
31271
+ const card = createSignInCard({
31272
+ tool,
31273
+ continuation,
31274
+ expiresAt,
31275
+ labels: {
31276
+ heading: "Sign in to continue",
31277
+ action: "Sign in"
31278
+ },
31279
+ onSignIn: () => this.#host.element.dispatchEvent(
31280
+ new CustomEvent("assistant-sign-in-requested", {
31281
+ detail: { id, tool, continuation, expiresAt },
31282
+ bubbles: true,
31283
+ composed: true
31284
+ })
31285
+ )
31286
+ });
31287
+ messages.append(card);
31288
+ this.#proposalCards.set(id, card);
31289
+ this.#host.revealLatest();
31290
+ this.#host.element.dispatchEvent(
31291
+ new CustomEvent("assistant-sign-in-required", {
31292
+ detail: { id, tool, continuation, expiresAt }
31293
+ })
31294
+ );
31295
+ }
31158
31296
  #appendInputRequest(id, message, requestedSchema) {
31159
31297
  const messages = this.#host.messages();
31160
31298
  if (!messages || !id) return;
@@ -31974,6 +32112,10 @@ var NoodleAssistantElement = class extends HTMLElementBase {
31974
32112
  #appearanceStyleSnapshot = /* @__PURE__ */ new Map();
31975
32113
  #scroll = new AssistantElementScrollController();
31976
32114
  sessionEndpoint = "";
32115
+ /** Public mount: the non-secret embed id `noodle deploy` printed. Mutually exclusive with the above. */
32116
+ embedId = "";
32117
+ /** Only for a dev or self-hosted service; the published snippet needs no origin. */
32118
+ serviceUrl = "";
31977
32119
  get appearance() {
31978
32120
  return this.#hostAppearance;
31979
32121
  }
@@ -32028,6 +32170,8 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32028
32170
  connectedCallback() {
32029
32171
  if (!this.shadowRoot) this.attachShadow({ mode: "open" });
32030
32172
  if (!this.sessionEndpoint) this.sessionEndpoint = this.getAttribute("session-endpoint") ?? "";
32173
+ if (!this.embedId) this.embedId = this.getAttribute("embed-id") ?? "";
32174
+ if (!this.serviceUrl) this.serviceUrl = this.getAttribute("service-url") ?? "";
32031
32175
  this.#media = globalThis.matchMedia?.("(prefers-color-scheme: dark)");
32032
32176
  this.#media?.addEventListener("change", this.#handleSystemTheme);
32033
32177
  document.addEventListener("keydown", this.#handleDocumentKeydown);
@@ -32199,26 +32343,31 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32199
32343
  }
32200
32344
  };
32201
32345
  #ensureClient() {
32202
- if (this.#client && this.#clientEndpoint === this.sessionEndpoint) return this.#client;
32346
+ const key = sessionSourceKey(this);
32347
+ if (this.#client && this.#clientEndpoint === key) return this.#client;
32203
32348
  this.#client?.abort();
32204
32349
  this.#unsubscribeClient?.();
32205
32350
  this.#sessionBootstrap = void 0;
32206
- const client = createAssistantClient({
32207
- sessionEndpoint: this.sessionEndpoint,
32351
+ const behavior = {
32208
32352
  ...this.#context ? { context: this.#context } : {},
32209
32353
  clientContext: browserClientContext,
32210
32354
  ...this.#pageContext ? { pageContext: this.#pageContext } : {},
32211
32355
  ...this.#modelContext ? { modelContext: this.#modelContext } : {}
32212
- });
32356
+ };
32357
+ const client = this.embedId ? createAssistantClient({
32358
+ embedId: this.embedId,
32359
+ ...this.serviceUrl ? { serviceUrl: this.serviceUrl } : {},
32360
+ ...behavior
32361
+ }) : createAssistantClient({ sessionEndpoint: this.sessionEndpoint, ...behavior });
32213
32362
  this.#client = client;
32214
- this.#clientEndpoint = this.sessionEndpoint;
32363
+ this.#clientEndpoint = key;
32215
32364
  this.#unsubscribeClient = client.subscribe((event) => {
32216
32365
  if (this.#client === client) this.#handleClientEvent(event);
32217
32366
  });
32218
32367
  return client;
32219
32368
  }
32220
32369
  #primeSession() {
32221
- if (!this.sessionEndpoint) return;
32370
+ if (!this.sessionEndpoint && !this.embedId) return;
32222
32371
  const client = this.#ensureClient();
32223
32372
  const generation = this.#conversationGeneration;
32224
32373
  this.#setSessionState("loading");
@@ -32342,23 +32491,30 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32342
32491
  }
32343
32492
  const detail = error51 instanceof AssistantClientError ? error51.detail : { code: fallbackCode, retryable: false };
32344
32493
  this.#appendRecoveryError(
32345
- detail.code === "session_expired" ? this.#appearance.labels.sessionExpired : this.#appearance.labels.unavailable
32494
+ detail.code === "session_expired" ? this.#appearance.labels.sessionExpired : this.#appearance.labels.unavailable,
32495
+ // A spent or switched-off daily budget offers no retry. Reconnecting cannot succeed, and every
32496
+ // open tab trying is exactly the load the cap was set to refuse — so the button goes, not just
32497
+ // the alarming words. A spent *session* keeps it: reconnect mints a fresh one, which is the fix.
32498
+ detail.retryable !== false || detail.serviceCode === void 0
32346
32499
  );
32347
32500
  this.#dispatchError(detail);
32348
32501
  }
32349
- #appendRecoveryError(message) {
32502
+ #appendRecoveryError(message, offerRetry = true) {
32350
32503
  if (!this.#messages) return;
32351
32504
  this.#messages.querySelector(".conversation-error")?.remove();
32352
32505
  const error51 = document.createElement("section");
32353
32506
  error51.className = "conversation-error";
32354
- error51.setAttribute("role", "alert");
32507
+ error51.setAttribute("role", offerRetry ? "alert" : "status");
32355
32508
  const text3 = document.createElement("p");
32356
32509
  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);
32510
+ error51.append(text3);
32511
+ if (offerRetry) {
32512
+ const retry = document.createElement("button");
32513
+ retry.type = "button";
32514
+ retry.textContent = this.#appearance.labels.reconnect;
32515
+ retry.addEventListener("click", () => this.reconnect());
32516
+ error51.append(retry);
32517
+ }
32362
32518
  this.#messages.append(error51);
32363
32519
  this.#revealLatest();
32364
32520
  }
@@ -32636,7 +32792,9 @@ var NoodleAssistantWithRef = (0, import_react2.forwardRef)(
32636
32792
  element.addEventListener("assistant-event", assistantEvent);
32637
32793
  element.addEventListener("assistant-appearance-warning", appearanceWarning);
32638
32794
  registerNoodleAssistant();
32639
- element.sessionEndpoint = props.sessionEndpoint;
32795
+ element.sessionEndpoint = props.sessionEndpoint ?? "";
32796
+ element.embedId = props.embedId ?? "";
32797
+ element.serviceUrl = props.serviceUrl ?? "";
32640
32798
  element.appearance = props.appearance;
32641
32799
  element.toggleAttribute("open", props.open === true);
32642
32800
  if (props.context) element.updateContext(props.context);
@@ -32661,12 +32819,16 @@ var NoodleAssistantWithRef = (0, import_react2.forwardRef)(
32661
32819
  props.onSessionExpired,
32662
32820
  props.open,
32663
32821
  props.pageContext,
32664
- props.sessionEndpoint
32822
+ props.sessionEndpoint,
32823
+ props.embedId,
32824
+ props.serviceUrl
32665
32825
  ]);
32666
32826
  return (0, import_react2.createElement)("noodle-assistant", {
32667
32827
  ref,
32668
32828
  class: props.className,
32669
- "session-endpoint": props.sessionEndpoint,
32829
+ ...props.sessionEndpoint ? { "session-endpoint": props.sessionEndpoint } : {},
32830
+ ...props.embedId ? { "embed-id": props.embedId } : {},
32831
+ ...props.serviceUrl ? { "service-url": props.serviceUrl } : {},
32670
32832
  ...props.theme ? { theme: props.theme } : {}
32671
32833
  });
32672
32834
  }