@noodleseed/assistant 1.28.0 → 1.30.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.
Files changed (42) hide show
  1. package/dist/{app-view-element-mXHvcAXc.d.ts → app-view-element-kR8eqhyD.d.cts} +1 -1
  2. package/dist/{app-view-element-BjRkwNN1.d.cts → app-view-element-nY1svwHT.d.ts} +1 -1
  3. package/dist/app-view.d.cts +3 -3
  4. package/dist/app-view.d.ts +3 -3
  5. package/dist/{chunk-WJV4MJUR.js → chunk-5EVZFPTD.js} +120 -10
  6. package/dist/chunk-5EVZFPTD.js.map +1 -0
  7. package/dist/{chunk-QH7PL3GU.js → chunk-T4PS6P2W.js} +259 -54
  8. package/dist/chunk-T4PS6P2W.js.map +1 -0
  9. package/dist/{client-CPFw8tfo.d.ts → client-contract-Bw9dtrhw.d.cts} +26 -35
  10. package/dist/{client-_xivfs4q.d.cts → client-contract-DjU9qsoP.d.ts} +26 -35
  11. package/dist/client.cjs +122 -9
  12. package/dist/client.cjs.map +1 -1
  13. package/dist/client.d.cts +12 -2
  14. package/dist/client.d.ts +12 -2
  15. package/dist/client.js +1 -1
  16. package/dist/embed.global.js +46 -46
  17. package/dist/index.cjs +376 -61
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +3 -3
  20. package/dist/index.d.ts +3 -3
  21. package/dist/index.js +2 -2
  22. package/dist/react/client.cjs +122 -9
  23. package/dist/react/client.cjs.map +1 -1
  24. package/dist/react/client.d.cts +2 -2
  25. package/dist/react/client.d.ts +2 -2
  26. package/dist/react/client.js +1 -1
  27. package/dist/react.cjs +376 -61
  28. package/dist/react.cjs.map +1 -1
  29. package/dist/react.d.cts +3 -3
  30. package/dist/react.d.ts +3 -3
  31. package/dist/react.js +2 -2
  32. package/dist/server-CMqlAnxs.d.cts +270 -0
  33. package/dist/server-CMqlAnxs.d.ts +270 -0
  34. package/dist/server.cjs.map +1 -1
  35. package/dist/server.d.cts +1 -121
  36. package/dist/server.d.ts +1 -121
  37. package/dist/server.js.map +1 -1
  38. package/package.json +1 -1
  39. package/dist/appearance-BpVu3lpk.d.cts +0 -146
  40. package/dist/appearance-BpVu3lpk.d.ts +0 -146
  41. package/dist/chunk-QH7PL3GU.js.map +0 -1
  42. package/dist/chunk-WJV4MJUR.js.map +0 -1
package/dist/react.cjs CHANGED
@@ -22485,11 +22485,8 @@ var DEFAULT_APPEARANCE = {
22485
22485
  composer: { leadingIcon: "none", sendIcon: "arrow-up", shape: "pill" },
22486
22486
  messages: { userStyle: "bubble", assistantStyle: "plain" }
22487
22487
  },
22488
- suggestedPrompts: [
22489
- "What products do you have?",
22490
- "Tell me about your business",
22491
- "How can I get in touch?"
22492
- ],
22488
+ suggestedPrompts: [],
22489
+ suggestedPromptsSource: "model",
22493
22490
  locale: "en",
22494
22491
  direction: "auto"
22495
22492
  };
@@ -22522,11 +22519,8 @@ function resolveAppearance(input = {}) {
22522
22519
  ...assistant?.labels
22523
22520
  },
22524
22521
  presentation: resolvePresentation(assistant?.presentation),
22525
- suggestedPrompts: assistant?.suggestedPrompts ?? (branding?.name ? [
22526
- "What products do you have?",
22527
- `Tell me about ${branding.name}`,
22528
- "How can I get in touch?"
22529
- ] : DEFAULT_APPEARANCE.suggestedPrompts),
22522
+ suggestedPrompts: assistant?.suggestedPrompts ?? [],
22523
+ suggestedPromptsSource: assistant?.suggestedPrompts === void 0 ? "model" : "configured",
22530
22524
  ...assistant?.privacyUrl ? { privacyUrl: assistant.privacyUrl } : {},
22531
22525
  ...assistant?.termsUrl ? { termsUrl: assistant.termsUrl } : {},
22532
22526
  locale: assistant?.locale ?? DEFAULT_APPEARANCE.locale,
@@ -27075,6 +27069,7 @@ var AssistantChatStateStore = class {
27075
27069
  #messages = [];
27076
27070
  #status = "ready";
27077
27071
  #error;
27072
+ #suggestions;
27078
27073
  #operation;
27079
27074
  #nextId = 0;
27080
27075
  subscribe(listener) {
@@ -27086,20 +27081,24 @@ var AssistantChatStateStore = class {
27086
27081
  return structuredClone({
27087
27082
  status: this.#status,
27088
27083
  messages: this.#messages,
27084
+ ...this.#suggestions ? { suggestions: this.#suggestions } : {},
27089
27085
  ...this.#error ? { error: this.#error } : {}
27090
27086
  });
27091
27087
  }
27092
27088
  handle(event) {
27093
27089
  switch (event.event) {
27094
27090
  case "message_started":
27091
+ this.#suggestions = void 0;
27095
27092
  this.#beginMessage(event.data.message);
27096
27093
  return;
27097
27094
  case "interaction_started":
27095
+ this.#suggestions = void 0;
27098
27096
  this.#beginInteraction(event.data.id);
27099
27097
  return;
27100
27098
  case "resume_started":
27101
27099
  this.#status = "submitted";
27102
27100
  this.#error = void 0;
27101
+ this.#suggestions = void 0;
27103
27102
  this.#startOperation();
27104
27103
  this.#notify();
27105
27104
  return;
@@ -27132,6 +27131,13 @@ var AssistantChatStateStore = class {
27132
27131
  case "view_available":
27133
27132
  this.#writeData("view", event.data.id, event.data);
27134
27133
  return;
27134
+ case "suggested_prompts":
27135
+ this.#suggestions = {
27136
+ phase: event.data.phase,
27137
+ prompts: [...event.data.prompts]
27138
+ };
27139
+ this.#notify();
27140
+ return;
27135
27141
  case "message_completed":
27136
27142
  this.#closeOperation("ready");
27137
27143
  return;
@@ -27172,6 +27178,7 @@ var AssistantChatStateStore = class {
27172
27178
  this.#messages = [...this.#messages, userMessage];
27173
27179
  this.#status = "submitted";
27174
27180
  this.#error = void 0;
27181
+ this.#suggestions = void 0;
27175
27182
  this.#startOperation();
27176
27183
  this.#notify();
27177
27184
  }
@@ -27501,7 +27508,8 @@ var assistantUiSchema = external_exports.object({
27501
27508
  privacyUrl: httpsUrlSchema.optional(),
27502
27509
  termsUrl: httpsUrlSchema.optional(),
27503
27510
  locale: external_exports.string().trim().min(2).max(35).optional(),
27504
- direction: external_exports.enum(["ltr", "rtl", "auto"]).optional()
27511
+ direction: external_exports.enum(["ltr", "rtl", "auto"]).optional(),
27512
+ webmcp: external_exports.object({ enabled: external_exports.boolean().optional() }).strict().optional()
27505
27513
  }).strict();
27506
27514
  var assistantConfigurationSchema = external_exports.object({ branding: brandingSchema.optional(), assistant: assistantUiSchema.optional() }).strict();
27507
27515
  function parseAssistantConfiguration(value) {
@@ -27563,7 +27571,8 @@ function parseSession(value) {
27563
27571
  const apps = value.endpoints.apps;
27564
27572
  const sandbox = value.endpoints.sandbox;
27565
27573
  const transcript = value.endpoints.transcript;
27566
- if (typeof value.token !== "string" || typeof value.expiresAt !== "string" || typeof value.endpoints.turns !== "string" || typeof value.endpoints.toolConfirmations !== "string" || interactions !== void 0 && typeof interactions !== "string" || apps !== void 0 && typeof apps !== "string" || sandbox !== void 0 && typeof sandbox !== "string" || transcript !== void 0 && typeof transcript !== "string") {
27574
+ const suggestions = value.endpoints.suggestions;
27575
+ if (typeof value.token !== "string" || typeof value.expiresAt !== "string" || typeof value.endpoints.turns !== "string" || typeof value.endpoints.toolConfirmations !== "string" || interactions !== void 0 && typeof interactions !== "string" || apps !== void 0 && typeof apps !== "string" || sandbox !== void 0 && typeof sandbox !== "string" || transcript !== void 0 && typeof transcript !== "string" || suggestions !== void 0 && typeof suggestions !== "string") {
27567
27576
  throw clientError("session_failed", "Assistant session response is invalid", true);
27568
27577
  }
27569
27578
  const configuration = parseAssistantConfiguration(value.configuration);
@@ -27576,7 +27585,8 @@ function parseSession(value) {
27576
27585
  ...typeof interactions === "string" ? { interactions } : {},
27577
27586
  ...typeof apps === "string" ? { apps } : {},
27578
27587
  ...typeof sandbox === "string" ? { sandbox } : {},
27579
- ...typeof transcript === "string" ? { transcript } : {}
27588
+ ...typeof transcript === "string" ? { transcript } : {},
27589
+ ...typeof suggestions === "string" ? { suggestions } : {}
27580
27590
  },
27581
27591
  ...configuration === void 0 ? {} : { configuration },
27582
27592
  ...isRecord2(value.resume) && typeof value.resume.tool === "string" ? { resume: { tool: value.resume.tool } } : {}
@@ -27660,6 +27670,10 @@ function toAssistantClientEvent(event) {
27660
27670
  return event;
27661
27671
  }
27662
27672
  break;
27673
+ case "suggested_prompts":
27674
+ if (isSuggestedPromptsDetail(value))
27675
+ return event;
27676
+ break;
27663
27677
  case "done":
27664
27678
  if (hasOptionalTurnId(value)) return event;
27665
27679
  break;
@@ -27671,6 +27685,12 @@ function toAssistantClientEvent(event) {
27671
27685
  }
27672
27686
  return { event: "unrecognized", data: { name: event.event, payload: value } };
27673
27687
  }
27688
+ function isSuggestedPromptsDetail(value) {
27689
+ if (!isRecord3(value)) return false;
27690
+ return (value.phase === "initial" || value.phase === "follow_up") && Array.isArray(value.prompts) && value.prompts.length <= 3 && value.prompts.every(
27691
+ (prompt) => typeof prompt === "string" && prompt.trim().length > 0 && prompt.length <= 240
27692
+ ) && hasOptionalTurnId(value);
27693
+ }
27674
27694
  function isViewAvailableDetail(value) {
27675
27695
  return hasString(value, "id") && hasString(value, "tool") && typeof value.resourceUri === "string" && value.resourceUri.startsWith("ui://") && hasOptionalString(value.title) && (value.replayed === void 0 || value.replayed === true) && isJsonValue(value.result, 0) && hasOptionalJson(value.arguments) && hasOptionalString(value.html) && (value.resourceMeta === void 0 || isRecord3(value.resourceMeta)) && (value.allowedOpenDomains === void 0 || Array.isArray(value.allowedOpenDomains) && value.allowedOpenDomains.every(isHttpsUrl2)) && hasOptionalTurnId(value);
27676
27696
  }
@@ -27749,6 +27769,11 @@ function parseTranscriptEntries(value) {
27749
27769
  }
27750
27770
  return parsed;
27751
27771
  }
27772
+ function parseTranscriptSuggestions(value) {
27773
+ if (typeof value !== "object" || value === null) return void 0;
27774
+ const suggestions = value.suggestions;
27775
+ return isSuggestedPromptsDetail(suggestions) && suggestions.phase === "follow_up" ? { phase: suggestions.phase, prompts: suggestions.prompts } : void 0;
27776
+ }
27752
27777
  function transcriptReplayEvents(entries2) {
27753
27778
  const events = [];
27754
27779
  let open = false;
@@ -27899,6 +27924,32 @@ function isAbortError2(error51) {
27899
27924
  return error51 instanceof DOMException && error51.name === "AbortError";
27900
27925
  }
27901
27926
 
27927
+ // src/visitor-id.ts
27928
+ var STORAGE_PREFIX = "noodleseed.assistant.visitor.";
27929
+ function randomVisitorId() {
27930
+ try {
27931
+ return globalThis.crypto.randomUUID();
27932
+ } catch {
27933
+ return void 0;
27934
+ }
27935
+ }
27936
+ function visitorIdForSource(sourceKey) {
27937
+ const key = `${STORAGE_PREFIX}${sourceKey}`;
27938
+ try {
27939
+ const stored = globalThis.localStorage?.getItem(key);
27940
+ if (typeof stored === "string" && stored.length > 0) return stored;
27941
+ } catch {
27942
+ return void 0;
27943
+ }
27944
+ const created = randomVisitorId();
27945
+ if (created === void 0) return void 0;
27946
+ try {
27947
+ globalThis.localStorage?.setItem(key, created);
27948
+ } catch {
27949
+ }
27950
+ return created;
27951
+ }
27952
+
27902
27953
  // src/client.ts
27903
27954
  var DefaultAssistantClient = class {
27904
27955
  #source;
@@ -27911,6 +27962,7 @@ var DefaultAssistantClient = class {
27911
27962
  #context;
27912
27963
  #modelContext;
27913
27964
  #active;
27965
+ #initialSuggestions;
27914
27966
  #pendingAppInteractions = /* @__PURE__ */ new Map();
27915
27967
  constructor(options) {
27916
27968
  this.#source = resolveSessionSource(options);
@@ -27972,10 +28024,13 @@ var DefaultAssistantClient = class {
27972
28024
  "turn_failed"
27973
28025
  );
27974
28026
  if (!response.ok) return;
27975
- for (const event of transcriptReplayEvents(parseTranscriptEntries(await response.json()))) {
28027
+ const value = await response.json();
28028
+ for (const event of transcriptReplayEvents(parseTranscriptEntries(value))) {
27976
28029
  this.#emit(event);
27977
28030
  if (event.event === "message_completed") await this.#chat.flush();
27978
28031
  }
28032
+ const suggestions = parseTranscriptSuggestions(value);
28033
+ if (suggestions) this.#emit({ event: "suggested_prompts", data: suggestions });
27979
28034
  }
27980
28035
  /** POST the one-shot resume trigger; a 409 means nothing was pending, which renders as silence. */
27981
28036
  async #resumeTurn(session, signal) {
@@ -27984,7 +28039,10 @@ var DefaultAssistantClient = class {
27984
28039
  {
27985
28040
  method: "POST",
27986
28041
  headers: authorizedHeaders(session.token, "text/event-stream"),
27987
- body: JSON.stringify({ resume: true }),
28042
+ body: JSON.stringify({
28043
+ resume: true,
28044
+ ...session.endpoints.suggestions ? { suggestions: true } : {}
28045
+ }),
27988
28046
  signal
27989
28047
  },
27990
28048
  "turn_failed"
@@ -28007,6 +28065,7 @@ var DefaultAssistantClient = class {
28007
28065
  async sendMessage(text3) {
28008
28066
  const message = text3.trim();
28009
28067
  if (!message) return;
28068
+ this.#cancelInitialSuggestions();
28010
28069
  await this.#singleFlight(async (signal) => {
28011
28070
  await this.#runChatOperation(
28012
28071
  signal,
@@ -28014,10 +28073,42 @@ var DefaultAssistantClient = class {
28014
28073
  );
28015
28074
  });
28016
28075
  }
28076
+ async loadInitialSuggestions() {
28077
+ if (!this.#session) await this.connect();
28078
+ const session = this.#session;
28079
+ if (!session?.endpoints.suggestions) return;
28080
+ if (this.#initialSuggestions) return this.#initialSuggestions.promise;
28081
+ const controller = new AbortController();
28082
+ const clientContext = this.#resolveClientContext();
28083
+ const pageContext = this.#resolvePageContext();
28084
+ const promise2 = this.#request(
28085
+ session.endpoints.suggestions,
28086
+ {
28087
+ method: "POST",
28088
+ headers: authorizedHeaders(session.token, "text/event-stream"),
28089
+ body: JSON.stringify({
28090
+ ...clientContext ? { clientContext } : {},
28091
+ ...pageContext === void 0 ? {} : { pageContext },
28092
+ ...this.#modelContext ? { modelContext: this.#modelContext } : {}
28093
+ }),
28094
+ signal: controller.signal
28095
+ },
28096
+ "turn_failed"
28097
+ ).then(async (response) => {
28098
+ if (response.ok) await this.#consume(response);
28099
+ }).finally(() => {
28100
+ if (this.#initialSuggestions?.controller === controller) {
28101
+ this.#initialSuggestions = void 0;
28102
+ }
28103
+ });
28104
+ this.#initialSuggestions = { controller, promise: promise2 };
28105
+ return promise2;
28106
+ }
28017
28107
  async respond(id, resolution) {
28018
28108
  if (!id) {
28019
28109
  throw clientError("invalid_request", "interaction id is required", false);
28020
28110
  }
28111
+ this.#cancelInitialSuggestions();
28021
28112
  await this.#singleFlight(async (signal) => {
28022
28113
  await this.#runChatOperation(signal, async () => {
28023
28114
  const pendingApp = this.#pendingAppInteractions.get(id);
@@ -28037,6 +28128,7 @@ var DefaultAssistantClient = class {
28037
28128
  const body = modernEndpoint ? {
28038
28129
  id,
28039
28130
  action: resolution.action,
28131
+ ...session.endpoints.suggestions ? { suggestions: true } : {},
28040
28132
  ...resolution.action === "accept" && resolution.content !== void 0 ? { content: resolution.content } : {}
28041
28133
  } : { id };
28042
28134
  this.#emit({ event: "interaction_started", data: { id, action: resolution.action } });
@@ -28115,7 +28207,7 @@ var DefaultAssistantClient = class {
28115
28207
  this.#modelContext = copyAssistantModelContext(update);
28116
28208
  this.#emit({ event: "model_context_changed", data: { modelContext: this.#modelContext } });
28117
28209
  }
28118
- async requestApp(method, params) {
28210
+ async requestApp(method, params, options) {
28119
28211
  const session = this.#session;
28120
28212
  if (!session?.endpoints.apps) {
28121
28213
  throw clientError(
@@ -28129,7 +28221,11 @@ var DefaultAssistantClient = class {
28129
28221
  {
28130
28222
  method: "POST",
28131
28223
  headers: authorizedHeaders(session.token, "application/json"),
28132
- body: JSON.stringify({ method, params })
28224
+ body: JSON.stringify({
28225
+ ...options?.bridge ? { bridge: options.bridge } : {},
28226
+ method,
28227
+ params
28228
+ })
28133
28229
  },
28134
28230
  "app_request_failed"
28135
28231
  );
@@ -28144,6 +28240,7 @@ var DefaultAssistantClient = class {
28144
28240
  const value = await response.json();
28145
28241
  const interaction = parseAppInteraction(value);
28146
28242
  if (!interaction) return value;
28243
+ options?.onSuspended?.();
28147
28244
  return new Promise((resolve2, reject) => {
28148
28245
  const id = interaction.data.id;
28149
28246
  const replaced = this.#pendingAppInteractions.get(id);
@@ -28161,6 +28258,7 @@ var DefaultAssistantClient = class {
28161
28258
  return this.#session?.endpoints.sandbox;
28162
28259
  }
28163
28260
  resetSession() {
28261
+ this.#cancelInitialSuggestions();
28164
28262
  this.#rejectPendingAppInteractions(
28165
28263
  clientError("confirmation_expired", "assistant session is not active", false)
28166
28264
  );
@@ -28169,6 +28267,7 @@ var DefaultAssistantClient = class {
28169
28267
  }
28170
28268
  abort() {
28171
28269
  this.#active?.abort();
28270
+ this.#cancelInitialSuggestions();
28172
28271
  }
28173
28272
  async #sendTurn(message, mayRetry, signal, modelContext) {
28174
28273
  const session = this.#session ?? await this.#createSession(signal);
@@ -28184,7 +28283,8 @@ var DefaultAssistantClient = class {
28184
28283
  message,
28185
28284
  ...clientContext ? { clientContext } : {},
28186
28285
  ...pageContext === void 0 ? {} : { pageContext },
28187
- ...modelContext ? { modelContext } : {}
28286
+ ...modelContext ? { modelContext } : {},
28287
+ ...session.endpoints.suggestions ? { suggestions: true } : {}
28188
28288
  }),
28189
28289
  signal
28190
28290
  },
@@ -28244,11 +28344,12 @@ var DefaultAssistantClient = class {
28244
28344
  }
28245
28345
  async #createSession(signal) {
28246
28346
  const source = this.#source;
28347
+ const visitorId = source.kind === "public" ? visitorIdForSource(sessionSourceKey({ embedId: source.embedId, serviceUrl: source.url })) : void 0;
28247
28348
  const response = await this.#requestSession(source, {
28248
28349
  method: "POST",
28249
28350
  headers: { Accept: "application/json", "Content-Type": "application/json" },
28250
28351
  body: JSON.stringify(
28251
- source.kind === "public" ? { embedId: source.embedId } : this.#context ? { context: this.#context } : {}
28352
+ source.kind === "public" ? { embedId: source.embedId, ...visitorId ? { visitorId } : {} } : this.#context ? { context: this.#context } : {}
28252
28353
  ),
28253
28354
  credentials: source.kind === "public" ? "omit" : "same-origin",
28254
28355
  signal
@@ -28373,6 +28474,9 @@ var DefaultAssistantClient = class {
28373
28474
  this.#active = void 0;
28374
28475
  }
28375
28476
  }
28477
+ #cancelInitialSuggestions() {
28478
+ this.#initialSuggestions?.controller.abort();
28479
+ }
28376
28480
  async #runChatOperation(signal, operation) {
28377
28481
  try {
28378
28482
  await operation();
@@ -28453,6 +28557,22 @@ function appendConversationRecovery(input) {
28453
28557
  input.revealLatest();
28454
28558
  }
28455
28559
 
28560
+ // src/element-error-recovery.ts
28561
+ function renderConversationError(options) {
28562
+ const { appearance, error: error51 } = options;
28563
+ const detail = error51 instanceof AssistantClientError ? error51.detail : { code: options.fallbackCode, retryable: false };
28564
+ const startNewConversation = detail.serviceCode === "session_turn_budget_exhausted";
28565
+ const reconnect = detail.retryable !== false || detail.serviceCode === void 0;
28566
+ const recoveryAction = startNewConversation ? { label: appearance.labels.newConversation, run: () => options.startNewConversation() } : reconnect ? { label: appearance.labels.reconnect, run: () => options.reconnect() } : void 0;
28567
+ appendConversationRecovery({
28568
+ messages: options.messages,
28569
+ message: detail.code === "session_expired" ? appearance.labels.sessionExpired : appearance.labels.unavailable,
28570
+ ...recoveryAction === void 0 ? {} : { action: recoveryAction },
28571
+ revealLatest: () => options.revealLatest()
28572
+ });
28573
+ return detail;
28574
+ }
28575
+
28456
28576
  // src/input-request-card.ts
28457
28577
  function createInputRequestCard(options) {
28458
28578
  const card = document.createElement("section");
@@ -32626,6 +32746,59 @@ var ASSISTANT_ELEMENT_STYLES = `<style>
32626
32746
  @media (max-width: 560px) { :host([data-mode="floating"]:not([open])) { bottom: 20px; } :host([open]:not([mobile-fullscreen])) .panel { width: calc(100vw - 40px); max-height: calc(100dvh - 40px); } }
32627
32747
  </style>`;
32628
32748
 
32749
+ // src/element-suggestions-controller.ts
32750
+ var AssistantElementSuggestionsController = class {
32751
+ #host;
32752
+ #initialRequested = false;
32753
+ #dynamic;
32754
+ constructor(host) {
32755
+ this.#host = host;
32756
+ }
32757
+ reset() {
32758
+ this.#initialRequested = false;
32759
+ this.clear();
32760
+ }
32761
+ clear() {
32762
+ this.#dynamic = void 0;
32763
+ this.render();
32764
+ }
32765
+ handle(event) {
32766
+ if (event.event === "message_started" || event.event === "interaction_started") {
32767
+ this.clear();
32768
+ return;
32769
+ }
32770
+ if (event.event !== "suggested_prompts") return;
32771
+ this.#dynamic = event.data;
32772
+ this.render();
32773
+ }
32774
+ maybeLoadInitial() {
32775
+ const appearance = this.#host.appearance();
32776
+ if (this.#initialRequested || appearance.suggestedPromptsSource !== "model" || this.#host.hasMessages() || !this.#host.isOpen()) {
32777
+ return;
32778
+ }
32779
+ const client = this.#host.client();
32780
+ if (!client?.loadInitialSuggestions) return;
32781
+ this.#initialRequested = true;
32782
+ void client.loadInitialSuggestions().catch(() => {
32783
+ });
32784
+ }
32785
+ render() {
32786
+ const container = this.#host.root()?.querySelector(".suggested-prompts");
32787
+ if (!container) return;
32788
+ const appearance = this.#host.appearance();
32789
+ const hasMessages = this.#host.hasMessages();
32790
+ const prompts = (this.#dynamic?.phase === "follow_up" || !hasMessages ? this.#dynamic?.prompts : void 0) ?? (!hasMessages && appearance.suggestedPromptsSource === "configured" ? appearance.suggestedPrompts : []);
32791
+ container.replaceChildren();
32792
+ for (const prompt of prompts) {
32793
+ const button2 = document.createElement("button");
32794
+ button2.type = "button";
32795
+ button2.textContent = prompt;
32796
+ button2.addEventListener("click", () => this.#host.send(prompt));
32797
+ container.append(button2);
32798
+ }
32799
+ }
32800
+ };
32801
+
32629
32802
  // src/host-appearance.ts
32630
32803
  var ROLE_VARIABLES = {
32631
32804
  canvas: "--ns-assistant-canvas",
@@ -32934,6 +33107,147 @@ function applyAssistantElementTheme(input) {
32934
33107
  return warnings;
32935
33108
  }
32936
33109
 
33110
+ // src/webmcp-bridge.ts
33111
+ var INERT = { registeredToolNames: [], stop: () => {
33112
+ } };
33113
+ async function startWebMcpBridge(ports) {
33114
+ const { client, modelContext, enabled } = ports;
33115
+ if (!enabled || !modelContext) return INERT;
33116
+ let listed;
33117
+ try {
33118
+ listed = await client.requestApp("tools/list", {});
33119
+ } catch {
33120
+ return INERT;
33121
+ }
33122
+ const registrations = [];
33123
+ for (const tool of projectableTools(listed)) {
33124
+ registrations.push({
33125
+ name: tool.name,
33126
+ handle: modelContext.registerTool({
33127
+ name: tool.name,
33128
+ ...tool.description === void 0 ? {} : { description: tool.description },
33129
+ ...tool.inputSchema === void 0 ? {} : { inputSchema: tool.inputSchema },
33130
+ // Passed through, never inferred: an agent may use `readOnlyHint` to decide it can call
33131
+ // without asking, so a hint this bridge invented would be a hint nobody authored.
33132
+ ...tool.annotations === void 0 ? {} : { annotations: tool.annotations },
33133
+ execute: (args) => executeThroughAssistant(client, tool.name, args)
33134
+ })
33135
+ });
33136
+ }
33137
+ let stopped = false;
33138
+ return {
33139
+ get registeredToolNames() {
33140
+ return stopped ? [] : registrations.map((entry) => entry.name);
33141
+ },
33142
+ stop() {
33143
+ if (stopped) return;
33144
+ stopped = true;
33145
+ for (const entry of registrations) entry.handle?.unregister?.();
33146
+ }
33147
+ };
33148
+ }
33149
+ function projectableTools(listed) {
33150
+ const tools = isRecord7(listed) && Array.isArray(listed.tools) ? listed.tools : [];
33151
+ const projectable = [];
33152
+ for (const tool of tools) {
33153
+ if (!isRecord7(tool) || typeof tool.name !== "string" || tool.name.length === 0) continue;
33154
+ const visibility = readVisibility(tool);
33155
+ if (visibility !== void 0 && !(visibility.includes("app") && visibility.includes("model"))) {
33156
+ continue;
33157
+ }
33158
+ projectable.push({
33159
+ name: tool.name,
33160
+ description: typeof tool.description === "string" ? tool.description : void 0,
33161
+ inputSchema: isRecord7(tool.inputSchema) ? tool.inputSchema : void 0,
33162
+ annotations: isRecord7(tool.annotations) ? tool.annotations : void 0
33163
+ });
33164
+ }
33165
+ return projectable;
33166
+ }
33167
+ function readVisibility(tool) {
33168
+ const meta3 = isRecord7(tool._meta) ? tool._meta : void 0;
33169
+ const ui = meta3 && isRecord7(meta3.ui) ? meta3.ui : void 0;
33170
+ const visibility = ui?.visibility;
33171
+ return Array.isArray(visibility) ? visibility.filter((entry) => typeof entry === "string") : void 0;
33172
+ }
33173
+ async function executeThroughAssistant(client, name21, args) {
33174
+ let suspend;
33175
+ const suspended = new Promise((resolve2) => {
33176
+ suspend = resolve2;
33177
+ });
33178
+ try {
33179
+ const call = client.requestApp(
33180
+ "tools/call",
33181
+ { name: name21, arguments: args },
33182
+ // Attribution only: anyone holding the session token could send this, and it selects a
33183
+ // *narrower* budget than the unmarked path, so declaring it can only cost the declarer.
33184
+ { bridge: "webmcp", onSuspended: suspend }
33185
+ );
33186
+ void call.catch(() => {
33187
+ });
33188
+ const outcome = await Promise.race([
33189
+ call.then((value) => ({ kind: "result", value })),
33190
+ suspended.then(() => ({ kind: "confirmation" }))
33191
+ ]);
33192
+ return outcome.kind === "confirmation" ? CONFIRMATION_REQUIRED : toToolResult(outcome.value);
33193
+ } catch (error51) {
33194
+ return textResult(error51 instanceof Error ? error51.message : "tool call failed", true);
33195
+ }
33196
+ }
33197
+ var CONFIRMATION_REQUIRED = {
33198
+ content: [
33199
+ {
33200
+ type: "text",
33201
+ text: "confirmation required: open the assistant panel on this page to approve this action"
33202
+ }
33203
+ ],
33204
+ isError: true
33205
+ };
33206
+ function toToolResult(value) {
33207
+ if (isRecord7(value) && Array.isArray(value.content)) return value;
33208
+ return textResult(JSON.stringify(value ?? null), false);
33209
+ }
33210
+ function textResult(text3, isError) {
33211
+ return { content: [{ type: "text", text: text3 }], isError };
33212
+ }
33213
+ function isRecord7(value) {
33214
+ return typeof value === "object" && value !== null && !Array.isArray(value);
33215
+ }
33216
+
33217
+ // src/element-webmcp-controller.ts
33218
+ var AssistantElementWebMcpController = class {
33219
+ #enabled = false;
33220
+ #bridge;
33221
+ /** Guards against a late `startWebMcpBridge` resolving after the session it belonged to is gone. */
33222
+ #generation = 0;
33223
+ /** Called when resolved configuration arrives; the deployment's opt-in is the only source. */
33224
+ configure(enabled) {
33225
+ this.#enabled = enabled;
33226
+ if (!enabled) this.stop();
33227
+ }
33228
+ start(client) {
33229
+ if (!this.#enabled || !client) return;
33230
+ const modelContext = browserModelContext();
33231
+ if (!modelContext) return;
33232
+ this.stop();
33233
+ const generation = ++this.#generation;
33234
+ void startWebMcpBridge({ client, modelContext, enabled: true }).then((bridge) => {
33235
+ if (generation === this.#generation) this.#bridge = bridge;
33236
+ else bridge.stop();
33237
+ });
33238
+ }
33239
+ stop() {
33240
+ this.#generation += 1;
33241
+ this.#bridge?.stop();
33242
+ this.#bridge = void 0;
33243
+ }
33244
+ };
33245
+ function browserModelContext() {
33246
+ if (typeof document === "undefined") return void 0;
33247
+ const host = document;
33248
+ return typeof host.modelContext?.registerTool === "function" ? host.modelContext : void 0;
33249
+ }
33250
+
32937
33251
  // src/presentation-styles.ts
32938
33252
  var presentationStyles = `<style>
32939
33253
  :host([data-mode="floating"]) {
@@ -33177,9 +33491,18 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33177
33491
  #turnInFlight = false;
33178
33492
  #fetch;
33179
33493
  #hostAppearance;
33494
+ #webmcp = new AssistantElementWebMcpController();
33180
33495
  #appearanceWarningKeys = /* @__PURE__ */ new Set();
33181
33496
  #appearanceStyleSnapshot = /* @__PURE__ */ new Map();
33182
33497
  #scroll = new AssistantElementScrollController();
33498
+ #suggestions = new AssistantElementSuggestionsController({
33499
+ root: () => this.shadowRoot,
33500
+ appearance: () => this.#appearance,
33501
+ client: () => this.#client,
33502
+ hasMessages: () => this.hasAttribute("has-messages"),
33503
+ isOpen: () => this.hasAttribute("open"),
33504
+ send: (prompt) => this.#sendGuarded(prompt)
33505
+ });
33183
33506
  sessionEndpoint = "";
33184
33507
  /** Public mount: the non-secret embed id `noodle deploy` printed. Mutually exclusive with the above. */
33185
33508
  embedId = "";
@@ -33291,6 +33614,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33291
33614
  this.#launcher.disconnect();
33292
33615
  this.#setBusy(false);
33293
33616
  this.#environment.disconnect();
33617
+ this.#webmcp.stop();
33294
33618
  }
33295
33619
  attributeChangedCallback(name21) {
33296
33620
  if (name21 === "theme") this.#applyTheme();
@@ -33324,6 +33648,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33324
33648
  }
33325
33649
  resetSession() {
33326
33650
  this.#conversationGeneration += 1;
33651
+ this.#webmcp.stop();
33327
33652
  this.#client?.abort();
33328
33653
  this.#unsubscribeClient?.();
33329
33654
  this.#client = void 0;
@@ -33333,12 +33658,14 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33333
33658
  this.#events.resetConversation();
33334
33659
  this.#pendingUserEcho = void 0;
33335
33660
  this.removeAttribute("has-messages");
33661
+ this.#suggestions.reset();
33336
33662
  this.#setBusy(false);
33337
33663
  this.#setSessionState("idle");
33338
33664
  }
33339
33665
  /** Re-establish the session without replaying a message or interaction decision. */
33340
33666
  reconnect() {
33341
33667
  this.#conversationGeneration += 1;
33668
+ this.#webmcp.stop();
33342
33669
  this.#client?.abort();
33343
33670
  this.#unsubscribeClient?.();
33344
33671
  this.#client = void 0;
@@ -33396,6 +33723,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33396
33723
  this.#turnInFlight = true;
33397
33724
  this.#pendingUserEcho = message;
33398
33725
  this.#appendMessage("user", message);
33726
+ this.#suggestions.clear();
33399
33727
  this.#setBusy(true);
33400
33728
  if (this.#sessionState !== "ready") this.#setSessionState("loading");
33401
33729
  try {
@@ -33415,12 +33743,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33415
33743
  if (generation === this.#conversationGeneration) this.#setBusy(false);
33416
33744
  }
33417
33745
  }
33418
- /**
33419
- * Every listener the element attaches to its own DOM must go through this guard: internal UI
33420
- * failures surface as `assistant-error` events (already dispatched by the failing path) and must
33421
- * never escape as unhandled rejections into the embedding page. Programmatic callers use
33422
- * `sendMessage`/`confirmTool` directly and keep the rejection.
33423
- */
33746
+ /** DOM listeners swallow after the failing path dispatches its typed assistant-error event. */
33424
33747
  #sendGuarded(text3) {
33425
33748
  void this.sendMessage(text3).catch(() => {
33426
33749
  });
@@ -33472,13 +33795,21 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33472
33795
  }
33473
33796
  #primeSession() {
33474
33797
  if (!this.sessionEndpoint && !this.embedId) return;
33475
- if (this.#sessionBootstrap || this.#client?.hasSession()) return;
33798
+ if (this.#sessionBootstrap) return;
33799
+ if (this.#client?.hasSession()) {
33800
+ this.#suggestions.maybeLoadInitial();
33801
+ return;
33802
+ }
33476
33803
  if (this.#client?.isBusy?.() === true) return;
33477
33804
  if (this.embedId && !this.#pageContext) void this.#automaticPageContext.refresh();
33478
33805
  const client = this.#ensureClient();
33479
33806
  const generation = this.#conversationGeneration;
33480
33807
  this.#setSessionState("loading");
33481
- const bootstrap = client.connect().catch((error51) => {
33808
+ const bootstrap = client.connect().then(() => {
33809
+ if (generation === this.#conversationGeneration && this.#client === client) {
33810
+ this.#suggestions.maybeLoadInitial();
33811
+ }
33812
+ }).catch((error51) => {
33482
33813
  if (generation !== this.#conversationGeneration || this.#client !== client) return;
33483
33814
  this.#dispatchClientError(error51, "session_failed");
33484
33815
  this.#presentationGate.reveal();
@@ -33490,15 +33821,18 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33490
33821
  #handleClientEvent(event) {
33491
33822
  this.dispatchEvent(new CustomEvent("assistant-event", { detail: event }));
33492
33823
  this.#events.handle(event);
33824
+ this.#suggestions.handle(event);
33493
33825
  if (event.event === "session_started") {
33494
33826
  this.#publicConfiguration.disconnect();
33495
33827
  this.removeAttribute("data-public-configuration-loading");
33496
33828
  this.#presentationGate.reveal();
33497
33829
  this.#setSessionState("ready");
33498
33830
  this.#messages?.querySelector(".conversation-error")?.remove();
33831
+ this.#webmcp.start(this.#client);
33499
33832
  }
33500
33833
  if (event.event === "session_expired" || event.event === "session_reset") {
33501
33834
  this.#setSessionState("idle");
33835
+ this.#webmcp.stop();
33502
33836
  }
33503
33837
  if (event.event === "tool_proposed" || event.event === "interaction_proposed" || event.event === "input_requested") {
33504
33838
  this.#removeThinking();
@@ -33602,23 +33936,17 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33602
33936
  if (error51 instanceof AssistantClientError && error51.detail.code === "session_failed") {
33603
33937
  this.#setSessionState("error");
33604
33938
  }
33605
- const detail = error51 instanceof AssistantClientError ? error51.detail : { code: fallbackCode, retryable: false };
33606
- const startNewConversation = detail.serviceCode === "session_turn_budget_exhausted";
33607
- const reconnect = detail.retryable !== false || detail.serviceCode === void 0;
33608
- const recoveryAction = startNewConversation ? {
33609
- label: this.#appearance.labels.newConversation,
33610
- run: () => this.#startNewConversation()
33611
- } : reconnect ? { label: this.#appearance.labels.reconnect, run: () => this.reconnect() } : void 0;
33612
- appendConversationRecovery({
33613
- messages: this.#messages,
33614
- message: detail.code === "session_expired" ? this.#appearance.labels.sessionExpired : this.#appearance.labels.unavailable,
33615
- // A spent or switched-off daily budget offers no retry. Reconnecting cannot succeed, and every
33616
- // open tab trying is exactly the load the cap was set to refuse — so the button goes, not just
33617
- // the alarming words. A spent *session* keeps it: reconnect mints a fresh one, which is the fix.
33618
- ...recoveryAction === void 0 ? {} : { action: recoveryAction },
33619
- revealLatest: () => this.#revealLatest()
33620
- });
33621
- this.#dispatchError(detail);
33939
+ this.#dispatchError(
33940
+ renderConversationError({
33941
+ messages: this.#messages,
33942
+ appearance: this.#appearance,
33943
+ error: error51,
33944
+ fallbackCode,
33945
+ startNewConversation: () => this.#startNewConversation(),
33946
+ reconnect: () => this.reconnect(),
33947
+ revealLatest: () => this.#revealLatest()
33948
+ })
33949
+ );
33622
33950
  }
33623
33951
  #startNewConversation() {
33624
33952
  this.resetSession();
@@ -33651,7 +33979,9 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33651
33979
  }
33652
33980
  }
33653
33981
  #applyConfiguration(configuration) {
33982
+ this.#webmcp.configure(configuration.assistant?.webmcp?.enabled === true);
33654
33983
  this.#appearance = resolveAppearance(configuration);
33984
+ this.#suggestions.render();
33655
33985
  this.#syncAppearance();
33656
33986
  this.#applyStartOpenPolicy();
33657
33987
  }
@@ -33696,15 +34026,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33696
34026
  "aria-label",
33697
34027
  appearance.labels.close
33698
34028
  );
33699
- const prompts = queryRequired(this.shadowRoot, ".suggested-prompts");
33700
- prompts.replaceChildren();
33701
- for (const prompt of appearance.suggestedPrompts) {
33702
- const button2 = document.createElement("button");
33703
- button2.type = "button";
33704
- button2.textContent = prompt;
33705
- button2.addEventListener("click", () => this.#sendGuarded(prompt));
33706
- prompts.append(button2);
33707
- }
34029
+ this.#suggestions.render();
33708
34030
  queryRequired(this.shadowRoot, "header").hidden = !appearance.behavior.showHeader;
33709
34031
  const legal = queryRequired(this.shadowRoot, ".legal");
33710
34032
  legal.replaceChildren();
@@ -33742,14 +34064,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33742
34064
  textarea.placeholder = appearance.labels.composerPlaceholder;
33743
34065
  textarea.setAttribute("aria-label", appearance.labels.composerPlaceholder);
33744
34066
  queryRequired(this.shadowRoot, ".send").textContent = appearance.labels.send;
33745
- const prompts = queryRequired(this.shadowRoot, ".suggested-prompts");
33746
- for (const prompt of appearance.suggestedPrompts) {
33747
- const button2 = document.createElement("button");
33748
- button2.type = "button";
33749
- button2.textContent = prompt;
33750
- button2.addEventListener("click", () => this.#sendGuarded(prompt));
33751
- prompts.append(button2);
33752
- }
34067
+ this.#suggestions.render();
33753
34068
  if (!appearance.behavior.showLauncher)
33754
34069
  queryRequired(this.shadowRoot, ".launcher").hidden = true;
33755
34070
  if (!appearance.behavior.showHeader)