@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/index.cjs CHANGED
@@ -22482,11 +22482,8 @@ var DEFAULT_APPEARANCE = {
22482
22482
  composer: { leadingIcon: "none", sendIcon: "arrow-up", shape: "pill" },
22483
22483
  messages: { userStyle: "bubble", assistantStyle: "plain" }
22484
22484
  },
22485
- suggestedPrompts: [
22486
- "What products do you have?",
22487
- "Tell me about your business",
22488
- "How can I get in touch?"
22489
- ],
22485
+ suggestedPrompts: [],
22486
+ suggestedPromptsSource: "model",
22490
22487
  locale: "en",
22491
22488
  direction: "auto"
22492
22489
  };
@@ -22519,11 +22516,8 @@ function resolveAppearance(input = {}) {
22519
22516
  ...assistant?.labels
22520
22517
  },
22521
22518
  presentation: resolvePresentation(assistant?.presentation),
22522
- suggestedPrompts: assistant?.suggestedPrompts ?? (branding?.name ? [
22523
- "What products do you have?",
22524
- `Tell me about ${branding.name}`,
22525
- "How can I get in touch?"
22526
- ] : DEFAULT_APPEARANCE.suggestedPrompts),
22519
+ suggestedPrompts: assistant?.suggestedPrompts ?? [],
22520
+ suggestedPromptsSource: assistant?.suggestedPrompts === void 0 ? "model" : "configured",
22527
22521
  ...assistant?.privacyUrl ? { privacyUrl: assistant.privacyUrl } : {},
22528
22522
  ...assistant?.termsUrl ? { termsUrl: assistant.termsUrl } : {},
22529
22523
  locale: assistant?.locale ?? DEFAULT_APPEARANCE.locale,
@@ -27072,6 +27066,7 @@ var AssistantChatStateStore = class {
27072
27066
  #messages = [];
27073
27067
  #status = "ready";
27074
27068
  #error;
27069
+ #suggestions;
27075
27070
  #operation;
27076
27071
  #nextId = 0;
27077
27072
  subscribe(listener) {
@@ -27083,20 +27078,24 @@ var AssistantChatStateStore = class {
27083
27078
  return structuredClone({
27084
27079
  status: this.#status,
27085
27080
  messages: this.#messages,
27081
+ ...this.#suggestions ? { suggestions: this.#suggestions } : {},
27086
27082
  ...this.#error ? { error: this.#error } : {}
27087
27083
  });
27088
27084
  }
27089
27085
  handle(event) {
27090
27086
  switch (event.event) {
27091
27087
  case "message_started":
27088
+ this.#suggestions = void 0;
27092
27089
  this.#beginMessage(event.data.message);
27093
27090
  return;
27094
27091
  case "interaction_started":
27092
+ this.#suggestions = void 0;
27095
27093
  this.#beginInteraction(event.data.id);
27096
27094
  return;
27097
27095
  case "resume_started":
27098
27096
  this.#status = "submitted";
27099
27097
  this.#error = void 0;
27098
+ this.#suggestions = void 0;
27100
27099
  this.#startOperation();
27101
27100
  this.#notify();
27102
27101
  return;
@@ -27129,6 +27128,13 @@ var AssistantChatStateStore = class {
27129
27128
  case "view_available":
27130
27129
  this.#writeData("view", event.data.id, event.data);
27131
27130
  return;
27131
+ case "suggested_prompts":
27132
+ this.#suggestions = {
27133
+ phase: event.data.phase,
27134
+ prompts: [...event.data.prompts]
27135
+ };
27136
+ this.#notify();
27137
+ return;
27132
27138
  case "message_completed":
27133
27139
  this.#closeOperation("ready");
27134
27140
  return;
@@ -27169,6 +27175,7 @@ var AssistantChatStateStore = class {
27169
27175
  this.#messages = [...this.#messages, userMessage];
27170
27176
  this.#status = "submitted";
27171
27177
  this.#error = void 0;
27178
+ this.#suggestions = void 0;
27172
27179
  this.#startOperation();
27173
27180
  this.#notify();
27174
27181
  }
@@ -27498,7 +27505,8 @@ var assistantUiSchema = external_exports.object({
27498
27505
  privacyUrl: httpsUrlSchema.optional(),
27499
27506
  termsUrl: httpsUrlSchema.optional(),
27500
27507
  locale: external_exports.string().trim().min(2).max(35).optional(),
27501
- direction: external_exports.enum(["ltr", "rtl", "auto"]).optional()
27508
+ direction: external_exports.enum(["ltr", "rtl", "auto"]).optional(),
27509
+ webmcp: external_exports.object({ enabled: external_exports.boolean().optional() }).strict().optional()
27502
27510
  }).strict();
27503
27511
  var assistantConfigurationSchema = external_exports.object({ branding: brandingSchema.optional(), assistant: assistantUiSchema.optional() }).strict();
27504
27512
  function parseAssistantConfiguration(value) {
@@ -27560,7 +27568,8 @@ function parseSession(value) {
27560
27568
  const apps = value.endpoints.apps;
27561
27569
  const sandbox = value.endpoints.sandbox;
27562
27570
  const transcript = value.endpoints.transcript;
27563
- 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") {
27571
+ const suggestions = value.endpoints.suggestions;
27572
+ 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") {
27564
27573
  throw clientError("session_failed", "Assistant session response is invalid", true);
27565
27574
  }
27566
27575
  const configuration = parseAssistantConfiguration(value.configuration);
@@ -27573,7 +27582,8 @@ function parseSession(value) {
27573
27582
  ...typeof interactions === "string" ? { interactions } : {},
27574
27583
  ...typeof apps === "string" ? { apps } : {},
27575
27584
  ...typeof sandbox === "string" ? { sandbox } : {},
27576
- ...typeof transcript === "string" ? { transcript } : {}
27585
+ ...typeof transcript === "string" ? { transcript } : {},
27586
+ ...typeof suggestions === "string" ? { suggestions } : {}
27577
27587
  },
27578
27588
  ...configuration === void 0 ? {} : { configuration },
27579
27589
  ...isRecord2(value.resume) && typeof value.resume.tool === "string" ? { resume: { tool: value.resume.tool } } : {}
@@ -27657,6 +27667,10 @@ function toAssistantClientEvent(event) {
27657
27667
  return event;
27658
27668
  }
27659
27669
  break;
27670
+ case "suggested_prompts":
27671
+ if (isSuggestedPromptsDetail(value))
27672
+ return event;
27673
+ break;
27660
27674
  case "done":
27661
27675
  if (hasOptionalTurnId(value)) return event;
27662
27676
  break;
@@ -27668,6 +27682,12 @@ function toAssistantClientEvent(event) {
27668
27682
  }
27669
27683
  return { event: "unrecognized", data: { name: event.event, payload: value } };
27670
27684
  }
27685
+ function isSuggestedPromptsDetail(value) {
27686
+ if (!isRecord3(value)) return false;
27687
+ return (value.phase === "initial" || value.phase === "follow_up") && Array.isArray(value.prompts) && value.prompts.length <= 3 && value.prompts.every(
27688
+ (prompt) => typeof prompt === "string" && prompt.trim().length > 0 && prompt.length <= 240
27689
+ ) && hasOptionalTurnId(value);
27690
+ }
27671
27691
  function isViewAvailableDetail(value) {
27672
27692
  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);
27673
27693
  }
@@ -27746,6 +27766,11 @@ function parseTranscriptEntries(value) {
27746
27766
  }
27747
27767
  return parsed;
27748
27768
  }
27769
+ function parseTranscriptSuggestions(value) {
27770
+ if (typeof value !== "object" || value === null) return void 0;
27771
+ const suggestions = value.suggestions;
27772
+ return isSuggestedPromptsDetail(suggestions) && suggestions.phase === "follow_up" ? { phase: suggestions.phase, prompts: suggestions.prompts } : void 0;
27773
+ }
27749
27774
  function transcriptReplayEvents(entries2) {
27750
27775
  const events = [];
27751
27776
  let open = false;
@@ -27896,6 +27921,32 @@ function isAbortError2(error51) {
27896
27921
  return error51 instanceof DOMException && error51.name === "AbortError";
27897
27922
  }
27898
27923
 
27924
+ // src/visitor-id.ts
27925
+ var STORAGE_PREFIX = "noodleseed.assistant.visitor.";
27926
+ function randomVisitorId() {
27927
+ try {
27928
+ return globalThis.crypto.randomUUID();
27929
+ } catch {
27930
+ return void 0;
27931
+ }
27932
+ }
27933
+ function visitorIdForSource(sourceKey) {
27934
+ const key = `${STORAGE_PREFIX}${sourceKey}`;
27935
+ try {
27936
+ const stored = globalThis.localStorage?.getItem(key);
27937
+ if (typeof stored === "string" && stored.length > 0) return stored;
27938
+ } catch {
27939
+ return void 0;
27940
+ }
27941
+ const created = randomVisitorId();
27942
+ if (created === void 0) return void 0;
27943
+ try {
27944
+ globalThis.localStorage?.setItem(key, created);
27945
+ } catch {
27946
+ }
27947
+ return created;
27948
+ }
27949
+
27899
27950
  // src/client.ts
27900
27951
  var DefaultAssistantClient = class {
27901
27952
  #source;
@@ -27908,6 +27959,7 @@ var DefaultAssistantClient = class {
27908
27959
  #context;
27909
27960
  #modelContext;
27910
27961
  #active;
27962
+ #initialSuggestions;
27911
27963
  #pendingAppInteractions = /* @__PURE__ */ new Map();
27912
27964
  constructor(options) {
27913
27965
  this.#source = resolveSessionSource(options);
@@ -27969,10 +28021,13 @@ var DefaultAssistantClient = class {
27969
28021
  "turn_failed"
27970
28022
  );
27971
28023
  if (!response.ok) return;
27972
- for (const event of transcriptReplayEvents(parseTranscriptEntries(await response.json()))) {
28024
+ const value = await response.json();
28025
+ for (const event of transcriptReplayEvents(parseTranscriptEntries(value))) {
27973
28026
  this.#emit(event);
27974
28027
  if (event.event === "message_completed") await this.#chat.flush();
27975
28028
  }
28029
+ const suggestions = parseTranscriptSuggestions(value);
28030
+ if (suggestions) this.#emit({ event: "suggested_prompts", data: suggestions });
27976
28031
  }
27977
28032
  /** POST the one-shot resume trigger; a 409 means nothing was pending, which renders as silence. */
27978
28033
  async #resumeTurn(session, signal) {
@@ -27981,7 +28036,10 @@ var DefaultAssistantClient = class {
27981
28036
  {
27982
28037
  method: "POST",
27983
28038
  headers: authorizedHeaders(session.token, "text/event-stream"),
27984
- body: JSON.stringify({ resume: true }),
28039
+ body: JSON.stringify({
28040
+ resume: true,
28041
+ ...session.endpoints.suggestions ? { suggestions: true } : {}
28042
+ }),
27985
28043
  signal
27986
28044
  },
27987
28045
  "turn_failed"
@@ -28004,6 +28062,7 @@ var DefaultAssistantClient = class {
28004
28062
  async sendMessage(text3) {
28005
28063
  const message = text3.trim();
28006
28064
  if (!message) return;
28065
+ this.#cancelInitialSuggestions();
28007
28066
  await this.#singleFlight(async (signal) => {
28008
28067
  await this.#runChatOperation(
28009
28068
  signal,
@@ -28011,10 +28070,42 @@ var DefaultAssistantClient = class {
28011
28070
  );
28012
28071
  });
28013
28072
  }
28073
+ async loadInitialSuggestions() {
28074
+ if (!this.#session) await this.connect();
28075
+ const session = this.#session;
28076
+ if (!session?.endpoints.suggestions) return;
28077
+ if (this.#initialSuggestions) return this.#initialSuggestions.promise;
28078
+ const controller = new AbortController();
28079
+ const clientContext = this.#resolveClientContext();
28080
+ const pageContext = this.#resolvePageContext();
28081
+ const promise2 = this.#request(
28082
+ session.endpoints.suggestions,
28083
+ {
28084
+ method: "POST",
28085
+ headers: authorizedHeaders(session.token, "text/event-stream"),
28086
+ body: JSON.stringify({
28087
+ ...clientContext ? { clientContext } : {},
28088
+ ...pageContext === void 0 ? {} : { pageContext },
28089
+ ...this.#modelContext ? { modelContext: this.#modelContext } : {}
28090
+ }),
28091
+ signal: controller.signal
28092
+ },
28093
+ "turn_failed"
28094
+ ).then(async (response) => {
28095
+ if (response.ok) await this.#consume(response);
28096
+ }).finally(() => {
28097
+ if (this.#initialSuggestions?.controller === controller) {
28098
+ this.#initialSuggestions = void 0;
28099
+ }
28100
+ });
28101
+ this.#initialSuggestions = { controller, promise: promise2 };
28102
+ return promise2;
28103
+ }
28014
28104
  async respond(id, resolution) {
28015
28105
  if (!id) {
28016
28106
  throw clientError("invalid_request", "interaction id is required", false);
28017
28107
  }
28108
+ this.#cancelInitialSuggestions();
28018
28109
  await this.#singleFlight(async (signal) => {
28019
28110
  await this.#runChatOperation(signal, async () => {
28020
28111
  const pendingApp = this.#pendingAppInteractions.get(id);
@@ -28034,6 +28125,7 @@ var DefaultAssistantClient = class {
28034
28125
  const body = modernEndpoint ? {
28035
28126
  id,
28036
28127
  action: resolution.action,
28128
+ ...session.endpoints.suggestions ? { suggestions: true } : {},
28037
28129
  ...resolution.action === "accept" && resolution.content !== void 0 ? { content: resolution.content } : {}
28038
28130
  } : { id };
28039
28131
  this.#emit({ event: "interaction_started", data: { id, action: resolution.action } });
@@ -28112,7 +28204,7 @@ var DefaultAssistantClient = class {
28112
28204
  this.#modelContext = copyAssistantModelContext(update);
28113
28205
  this.#emit({ event: "model_context_changed", data: { modelContext: this.#modelContext } });
28114
28206
  }
28115
- async requestApp(method, params) {
28207
+ async requestApp(method, params, options) {
28116
28208
  const session = this.#session;
28117
28209
  if (!session?.endpoints.apps) {
28118
28210
  throw clientError(
@@ -28126,7 +28218,11 @@ var DefaultAssistantClient = class {
28126
28218
  {
28127
28219
  method: "POST",
28128
28220
  headers: authorizedHeaders(session.token, "application/json"),
28129
- body: JSON.stringify({ method, params })
28221
+ body: JSON.stringify({
28222
+ ...options?.bridge ? { bridge: options.bridge } : {},
28223
+ method,
28224
+ params
28225
+ })
28130
28226
  },
28131
28227
  "app_request_failed"
28132
28228
  );
@@ -28141,6 +28237,7 @@ var DefaultAssistantClient = class {
28141
28237
  const value = await response.json();
28142
28238
  const interaction = parseAppInteraction(value);
28143
28239
  if (!interaction) return value;
28240
+ options?.onSuspended?.();
28144
28241
  return new Promise((resolve2, reject) => {
28145
28242
  const id = interaction.data.id;
28146
28243
  const replaced = this.#pendingAppInteractions.get(id);
@@ -28158,6 +28255,7 @@ var DefaultAssistantClient = class {
28158
28255
  return this.#session?.endpoints.sandbox;
28159
28256
  }
28160
28257
  resetSession() {
28258
+ this.#cancelInitialSuggestions();
28161
28259
  this.#rejectPendingAppInteractions(
28162
28260
  clientError("confirmation_expired", "assistant session is not active", false)
28163
28261
  );
@@ -28166,6 +28264,7 @@ var DefaultAssistantClient = class {
28166
28264
  }
28167
28265
  abort() {
28168
28266
  this.#active?.abort();
28267
+ this.#cancelInitialSuggestions();
28169
28268
  }
28170
28269
  async #sendTurn(message, mayRetry, signal, modelContext) {
28171
28270
  const session = this.#session ?? await this.#createSession(signal);
@@ -28181,7 +28280,8 @@ var DefaultAssistantClient = class {
28181
28280
  message,
28182
28281
  ...clientContext ? { clientContext } : {},
28183
28282
  ...pageContext === void 0 ? {} : { pageContext },
28184
- ...modelContext ? { modelContext } : {}
28283
+ ...modelContext ? { modelContext } : {},
28284
+ ...session.endpoints.suggestions ? { suggestions: true } : {}
28185
28285
  }),
28186
28286
  signal
28187
28287
  },
@@ -28241,11 +28341,12 @@ var DefaultAssistantClient = class {
28241
28341
  }
28242
28342
  async #createSession(signal) {
28243
28343
  const source = this.#source;
28344
+ const visitorId = source.kind === "public" ? visitorIdForSource(sessionSourceKey({ embedId: source.embedId, serviceUrl: source.url })) : void 0;
28244
28345
  const response = await this.#requestSession(source, {
28245
28346
  method: "POST",
28246
28347
  headers: { Accept: "application/json", "Content-Type": "application/json" },
28247
28348
  body: JSON.stringify(
28248
- source.kind === "public" ? { embedId: source.embedId } : this.#context ? { context: this.#context } : {}
28349
+ source.kind === "public" ? { embedId: source.embedId, ...visitorId ? { visitorId } : {} } : this.#context ? { context: this.#context } : {}
28249
28350
  ),
28250
28351
  credentials: source.kind === "public" ? "omit" : "same-origin",
28251
28352
  signal
@@ -28370,6 +28471,9 @@ var DefaultAssistantClient = class {
28370
28471
  this.#active = void 0;
28371
28472
  }
28372
28473
  }
28474
+ #cancelInitialSuggestions() {
28475
+ this.#initialSuggestions?.controller.abort();
28476
+ }
28373
28477
  async #runChatOperation(signal, operation) {
28374
28478
  try {
28375
28479
  await operation();
@@ -28450,6 +28554,22 @@ function appendConversationRecovery(input) {
28450
28554
  input.revealLatest();
28451
28555
  }
28452
28556
 
28557
+ // src/element-error-recovery.ts
28558
+ function renderConversationError(options) {
28559
+ const { appearance, error: error51 } = options;
28560
+ const detail = error51 instanceof AssistantClientError ? error51.detail : { code: options.fallbackCode, retryable: false };
28561
+ const startNewConversation = detail.serviceCode === "session_turn_budget_exhausted";
28562
+ const reconnect = detail.retryable !== false || detail.serviceCode === void 0;
28563
+ const recoveryAction = startNewConversation ? { label: appearance.labels.newConversation, run: () => options.startNewConversation() } : reconnect ? { label: appearance.labels.reconnect, run: () => options.reconnect() } : void 0;
28564
+ appendConversationRecovery({
28565
+ messages: options.messages,
28566
+ message: detail.code === "session_expired" ? appearance.labels.sessionExpired : appearance.labels.unavailable,
28567
+ ...recoveryAction === void 0 ? {} : { action: recoveryAction },
28568
+ revealLatest: () => options.revealLatest()
28569
+ });
28570
+ return detail;
28571
+ }
28572
+
28453
28573
  // src/input-request-card.ts
28454
28574
  function createInputRequestCard(options) {
28455
28575
  const card = document.createElement("section");
@@ -32623,6 +32743,59 @@ var ASSISTANT_ELEMENT_STYLES = `<style>
32623
32743
  @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); } }
32624
32744
  </style>`;
32625
32745
 
32746
+ // src/element-suggestions-controller.ts
32747
+ var AssistantElementSuggestionsController = class {
32748
+ #host;
32749
+ #initialRequested = false;
32750
+ #dynamic;
32751
+ constructor(host) {
32752
+ this.#host = host;
32753
+ }
32754
+ reset() {
32755
+ this.#initialRequested = false;
32756
+ this.clear();
32757
+ }
32758
+ clear() {
32759
+ this.#dynamic = void 0;
32760
+ this.render();
32761
+ }
32762
+ handle(event) {
32763
+ if (event.event === "message_started" || event.event === "interaction_started") {
32764
+ this.clear();
32765
+ return;
32766
+ }
32767
+ if (event.event !== "suggested_prompts") return;
32768
+ this.#dynamic = event.data;
32769
+ this.render();
32770
+ }
32771
+ maybeLoadInitial() {
32772
+ const appearance = this.#host.appearance();
32773
+ if (this.#initialRequested || appearance.suggestedPromptsSource !== "model" || this.#host.hasMessages() || !this.#host.isOpen()) {
32774
+ return;
32775
+ }
32776
+ const client = this.#host.client();
32777
+ if (!client?.loadInitialSuggestions) return;
32778
+ this.#initialRequested = true;
32779
+ void client.loadInitialSuggestions().catch(() => {
32780
+ });
32781
+ }
32782
+ render() {
32783
+ const container = this.#host.root()?.querySelector(".suggested-prompts");
32784
+ if (!container) return;
32785
+ const appearance = this.#host.appearance();
32786
+ const hasMessages = this.#host.hasMessages();
32787
+ const prompts = (this.#dynamic?.phase === "follow_up" || !hasMessages ? this.#dynamic?.prompts : void 0) ?? (!hasMessages && appearance.suggestedPromptsSource === "configured" ? appearance.suggestedPrompts : []);
32788
+ container.replaceChildren();
32789
+ for (const prompt of prompts) {
32790
+ const button2 = document.createElement("button");
32791
+ button2.type = "button";
32792
+ button2.textContent = prompt;
32793
+ button2.addEventListener("click", () => this.#host.send(prompt));
32794
+ container.append(button2);
32795
+ }
32796
+ }
32797
+ };
32798
+
32626
32799
  // src/host-appearance.ts
32627
32800
  var ROLE_VARIABLES = {
32628
32801
  canvas: "--ns-assistant-canvas",
@@ -32931,6 +33104,147 @@ function applyAssistantElementTheme(input) {
32931
33104
  return warnings;
32932
33105
  }
32933
33106
 
33107
+ // src/webmcp-bridge.ts
33108
+ var INERT = { registeredToolNames: [], stop: () => {
33109
+ } };
33110
+ async function startWebMcpBridge(ports) {
33111
+ const { client, modelContext, enabled } = ports;
33112
+ if (!enabled || !modelContext) return INERT;
33113
+ let listed;
33114
+ try {
33115
+ listed = await client.requestApp("tools/list", {});
33116
+ } catch {
33117
+ return INERT;
33118
+ }
33119
+ const registrations = [];
33120
+ for (const tool of projectableTools(listed)) {
33121
+ registrations.push({
33122
+ name: tool.name,
33123
+ handle: modelContext.registerTool({
33124
+ name: tool.name,
33125
+ ...tool.description === void 0 ? {} : { description: tool.description },
33126
+ ...tool.inputSchema === void 0 ? {} : { inputSchema: tool.inputSchema },
33127
+ // Passed through, never inferred: an agent may use `readOnlyHint` to decide it can call
33128
+ // without asking, so a hint this bridge invented would be a hint nobody authored.
33129
+ ...tool.annotations === void 0 ? {} : { annotations: tool.annotations },
33130
+ execute: (args) => executeThroughAssistant(client, tool.name, args)
33131
+ })
33132
+ });
33133
+ }
33134
+ let stopped = false;
33135
+ return {
33136
+ get registeredToolNames() {
33137
+ return stopped ? [] : registrations.map((entry) => entry.name);
33138
+ },
33139
+ stop() {
33140
+ if (stopped) return;
33141
+ stopped = true;
33142
+ for (const entry of registrations) entry.handle?.unregister?.();
33143
+ }
33144
+ };
33145
+ }
33146
+ function projectableTools(listed) {
33147
+ const tools = isRecord7(listed) && Array.isArray(listed.tools) ? listed.tools : [];
33148
+ const projectable = [];
33149
+ for (const tool of tools) {
33150
+ if (!isRecord7(tool) || typeof tool.name !== "string" || tool.name.length === 0) continue;
33151
+ const visibility = readVisibility(tool);
33152
+ if (visibility !== void 0 && !(visibility.includes("app") && visibility.includes("model"))) {
33153
+ continue;
33154
+ }
33155
+ projectable.push({
33156
+ name: tool.name,
33157
+ description: typeof tool.description === "string" ? tool.description : void 0,
33158
+ inputSchema: isRecord7(tool.inputSchema) ? tool.inputSchema : void 0,
33159
+ annotations: isRecord7(tool.annotations) ? tool.annotations : void 0
33160
+ });
33161
+ }
33162
+ return projectable;
33163
+ }
33164
+ function readVisibility(tool) {
33165
+ const meta3 = isRecord7(tool._meta) ? tool._meta : void 0;
33166
+ const ui = meta3 && isRecord7(meta3.ui) ? meta3.ui : void 0;
33167
+ const visibility = ui?.visibility;
33168
+ return Array.isArray(visibility) ? visibility.filter((entry) => typeof entry === "string") : void 0;
33169
+ }
33170
+ async function executeThroughAssistant(client, name21, args) {
33171
+ let suspend;
33172
+ const suspended = new Promise((resolve2) => {
33173
+ suspend = resolve2;
33174
+ });
33175
+ try {
33176
+ const call = client.requestApp(
33177
+ "tools/call",
33178
+ { name: name21, arguments: args },
33179
+ // Attribution only: anyone holding the session token could send this, and it selects a
33180
+ // *narrower* budget than the unmarked path, so declaring it can only cost the declarer.
33181
+ { bridge: "webmcp", onSuspended: suspend }
33182
+ );
33183
+ void call.catch(() => {
33184
+ });
33185
+ const outcome = await Promise.race([
33186
+ call.then((value) => ({ kind: "result", value })),
33187
+ suspended.then(() => ({ kind: "confirmation" }))
33188
+ ]);
33189
+ return outcome.kind === "confirmation" ? CONFIRMATION_REQUIRED : toToolResult(outcome.value);
33190
+ } catch (error51) {
33191
+ return textResult(error51 instanceof Error ? error51.message : "tool call failed", true);
33192
+ }
33193
+ }
33194
+ var CONFIRMATION_REQUIRED = {
33195
+ content: [
33196
+ {
33197
+ type: "text",
33198
+ text: "confirmation required: open the assistant panel on this page to approve this action"
33199
+ }
33200
+ ],
33201
+ isError: true
33202
+ };
33203
+ function toToolResult(value) {
33204
+ if (isRecord7(value) && Array.isArray(value.content)) return value;
33205
+ return textResult(JSON.stringify(value ?? null), false);
33206
+ }
33207
+ function textResult(text3, isError) {
33208
+ return { content: [{ type: "text", text: text3 }], isError };
33209
+ }
33210
+ function isRecord7(value) {
33211
+ return typeof value === "object" && value !== null && !Array.isArray(value);
33212
+ }
33213
+
33214
+ // src/element-webmcp-controller.ts
33215
+ var AssistantElementWebMcpController = class {
33216
+ #enabled = false;
33217
+ #bridge;
33218
+ /** Guards against a late `startWebMcpBridge` resolving after the session it belonged to is gone. */
33219
+ #generation = 0;
33220
+ /** Called when resolved configuration arrives; the deployment's opt-in is the only source. */
33221
+ configure(enabled) {
33222
+ this.#enabled = enabled;
33223
+ if (!enabled) this.stop();
33224
+ }
33225
+ start(client) {
33226
+ if (!this.#enabled || !client) return;
33227
+ const modelContext = browserModelContext();
33228
+ if (!modelContext) return;
33229
+ this.stop();
33230
+ const generation = ++this.#generation;
33231
+ void startWebMcpBridge({ client, modelContext, enabled: true }).then((bridge) => {
33232
+ if (generation === this.#generation) this.#bridge = bridge;
33233
+ else bridge.stop();
33234
+ });
33235
+ }
33236
+ stop() {
33237
+ this.#generation += 1;
33238
+ this.#bridge?.stop();
33239
+ this.#bridge = void 0;
33240
+ }
33241
+ };
33242
+ function browserModelContext() {
33243
+ if (typeof document === "undefined") return void 0;
33244
+ const host = document;
33245
+ return typeof host.modelContext?.registerTool === "function" ? host.modelContext : void 0;
33246
+ }
33247
+
32934
33248
  // src/presentation-styles.ts
32935
33249
  var presentationStyles = `<style>
32936
33250
  :host([data-mode="floating"]) {
@@ -33174,9 +33488,18 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33174
33488
  #turnInFlight = false;
33175
33489
  #fetch;
33176
33490
  #hostAppearance;
33491
+ #webmcp = new AssistantElementWebMcpController();
33177
33492
  #appearanceWarningKeys = /* @__PURE__ */ new Set();
33178
33493
  #appearanceStyleSnapshot = /* @__PURE__ */ new Map();
33179
33494
  #scroll = new AssistantElementScrollController();
33495
+ #suggestions = new AssistantElementSuggestionsController({
33496
+ root: () => this.shadowRoot,
33497
+ appearance: () => this.#appearance,
33498
+ client: () => this.#client,
33499
+ hasMessages: () => this.hasAttribute("has-messages"),
33500
+ isOpen: () => this.hasAttribute("open"),
33501
+ send: (prompt) => this.#sendGuarded(prompt)
33502
+ });
33180
33503
  sessionEndpoint = "";
33181
33504
  /** Public mount: the non-secret embed id `noodle deploy` printed. Mutually exclusive with the above. */
33182
33505
  embedId = "";
@@ -33288,6 +33611,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33288
33611
  this.#launcher.disconnect();
33289
33612
  this.#setBusy(false);
33290
33613
  this.#environment.disconnect();
33614
+ this.#webmcp.stop();
33291
33615
  }
33292
33616
  attributeChangedCallback(name21) {
33293
33617
  if (name21 === "theme") this.#applyTheme();
@@ -33321,6 +33645,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33321
33645
  }
33322
33646
  resetSession() {
33323
33647
  this.#conversationGeneration += 1;
33648
+ this.#webmcp.stop();
33324
33649
  this.#client?.abort();
33325
33650
  this.#unsubscribeClient?.();
33326
33651
  this.#client = void 0;
@@ -33330,12 +33655,14 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33330
33655
  this.#events.resetConversation();
33331
33656
  this.#pendingUserEcho = void 0;
33332
33657
  this.removeAttribute("has-messages");
33658
+ this.#suggestions.reset();
33333
33659
  this.#setBusy(false);
33334
33660
  this.#setSessionState("idle");
33335
33661
  }
33336
33662
  /** Re-establish the session without replaying a message or interaction decision. */
33337
33663
  reconnect() {
33338
33664
  this.#conversationGeneration += 1;
33665
+ this.#webmcp.stop();
33339
33666
  this.#client?.abort();
33340
33667
  this.#unsubscribeClient?.();
33341
33668
  this.#client = void 0;
@@ -33393,6 +33720,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33393
33720
  this.#turnInFlight = true;
33394
33721
  this.#pendingUserEcho = message;
33395
33722
  this.#appendMessage("user", message);
33723
+ this.#suggestions.clear();
33396
33724
  this.#setBusy(true);
33397
33725
  if (this.#sessionState !== "ready") this.#setSessionState("loading");
33398
33726
  try {
@@ -33412,12 +33740,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33412
33740
  if (generation === this.#conversationGeneration) this.#setBusy(false);
33413
33741
  }
33414
33742
  }
33415
- /**
33416
- * Every listener the element attaches to its own DOM must go through this guard: internal UI
33417
- * failures surface as `assistant-error` events (already dispatched by the failing path) and must
33418
- * never escape as unhandled rejections into the embedding page. Programmatic callers use
33419
- * `sendMessage`/`confirmTool` directly and keep the rejection.
33420
- */
33743
+ /** DOM listeners swallow after the failing path dispatches its typed assistant-error event. */
33421
33744
  #sendGuarded(text3) {
33422
33745
  void this.sendMessage(text3).catch(() => {
33423
33746
  });
@@ -33469,13 +33792,21 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33469
33792
  }
33470
33793
  #primeSession() {
33471
33794
  if (!this.sessionEndpoint && !this.embedId) return;
33472
- if (this.#sessionBootstrap || this.#client?.hasSession()) return;
33795
+ if (this.#sessionBootstrap) return;
33796
+ if (this.#client?.hasSession()) {
33797
+ this.#suggestions.maybeLoadInitial();
33798
+ return;
33799
+ }
33473
33800
  if (this.#client?.isBusy?.() === true) return;
33474
33801
  if (this.embedId && !this.#pageContext) void this.#automaticPageContext.refresh();
33475
33802
  const client = this.#ensureClient();
33476
33803
  const generation = this.#conversationGeneration;
33477
33804
  this.#setSessionState("loading");
33478
- const bootstrap = client.connect().catch((error51) => {
33805
+ const bootstrap = client.connect().then(() => {
33806
+ if (generation === this.#conversationGeneration && this.#client === client) {
33807
+ this.#suggestions.maybeLoadInitial();
33808
+ }
33809
+ }).catch((error51) => {
33479
33810
  if (generation !== this.#conversationGeneration || this.#client !== client) return;
33480
33811
  this.#dispatchClientError(error51, "session_failed");
33481
33812
  this.#presentationGate.reveal();
@@ -33487,15 +33818,18 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33487
33818
  #handleClientEvent(event) {
33488
33819
  this.dispatchEvent(new CustomEvent("assistant-event", { detail: event }));
33489
33820
  this.#events.handle(event);
33821
+ this.#suggestions.handle(event);
33490
33822
  if (event.event === "session_started") {
33491
33823
  this.#publicConfiguration.disconnect();
33492
33824
  this.removeAttribute("data-public-configuration-loading");
33493
33825
  this.#presentationGate.reveal();
33494
33826
  this.#setSessionState("ready");
33495
33827
  this.#messages?.querySelector(".conversation-error")?.remove();
33828
+ this.#webmcp.start(this.#client);
33496
33829
  }
33497
33830
  if (event.event === "session_expired" || event.event === "session_reset") {
33498
33831
  this.#setSessionState("idle");
33832
+ this.#webmcp.stop();
33499
33833
  }
33500
33834
  if (event.event === "tool_proposed" || event.event === "interaction_proposed" || event.event === "input_requested") {
33501
33835
  this.#removeThinking();
@@ -33599,23 +33933,17 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33599
33933
  if (error51 instanceof AssistantClientError && error51.detail.code === "session_failed") {
33600
33934
  this.#setSessionState("error");
33601
33935
  }
33602
- const detail = error51 instanceof AssistantClientError ? error51.detail : { code: fallbackCode, retryable: false };
33603
- const startNewConversation = detail.serviceCode === "session_turn_budget_exhausted";
33604
- const reconnect = detail.retryable !== false || detail.serviceCode === void 0;
33605
- const recoveryAction = startNewConversation ? {
33606
- label: this.#appearance.labels.newConversation,
33607
- run: () => this.#startNewConversation()
33608
- } : reconnect ? { label: this.#appearance.labels.reconnect, run: () => this.reconnect() } : void 0;
33609
- appendConversationRecovery({
33610
- messages: this.#messages,
33611
- message: detail.code === "session_expired" ? this.#appearance.labels.sessionExpired : this.#appearance.labels.unavailable,
33612
- // A spent or switched-off daily budget offers no retry. Reconnecting cannot succeed, and every
33613
- // open tab trying is exactly the load the cap was set to refuse — so the button goes, not just
33614
- // the alarming words. A spent *session* keeps it: reconnect mints a fresh one, which is the fix.
33615
- ...recoveryAction === void 0 ? {} : { action: recoveryAction },
33616
- revealLatest: () => this.#revealLatest()
33617
- });
33618
- this.#dispatchError(detail);
33936
+ this.#dispatchError(
33937
+ renderConversationError({
33938
+ messages: this.#messages,
33939
+ appearance: this.#appearance,
33940
+ error: error51,
33941
+ fallbackCode,
33942
+ startNewConversation: () => this.#startNewConversation(),
33943
+ reconnect: () => this.reconnect(),
33944
+ revealLatest: () => this.#revealLatest()
33945
+ })
33946
+ );
33619
33947
  }
33620
33948
  #startNewConversation() {
33621
33949
  this.resetSession();
@@ -33648,7 +33976,9 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33648
33976
  }
33649
33977
  }
33650
33978
  #applyConfiguration(configuration) {
33979
+ this.#webmcp.configure(configuration.assistant?.webmcp?.enabled === true);
33651
33980
  this.#appearance = resolveAppearance(configuration);
33981
+ this.#suggestions.render();
33652
33982
  this.#syncAppearance();
33653
33983
  this.#applyStartOpenPolicy();
33654
33984
  }
@@ -33693,15 +34023,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33693
34023
  "aria-label",
33694
34024
  appearance.labels.close
33695
34025
  );
33696
- const prompts = queryRequired(this.shadowRoot, ".suggested-prompts");
33697
- prompts.replaceChildren();
33698
- for (const prompt of appearance.suggestedPrompts) {
33699
- const button2 = document.createElement("button");
33700
- button2.type = "button";
33701
- button2.textContent = prompt;
33702
- button2.addEventListener("click", () => this.#sendGuarded(prompt));
33703
- prompts.append(button2);
33704
- }
34026
+ this.#suggestions.render();
33705
34027
  queryRequired(this.shadowRoot, "header").hidden = !appearance.behavior.showHeader;
33706
34028
  const legal = queryRequired(this.shadowRoot, ".legal");
33707
34029
  legal.replaceChildren();
@@ -33739,14 +34061,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33739
34061
  textarea.placeholder = appearance.labels.composerPlaceholder;
33740
34062
  textarea.setAttribute("aria-label", appearance.labels.composerPlaceholder);
33741
34063
  queryRequired(this.shadowRoot, ".send").textContent = appearance.labels.send;
33742
- const prompts = queryRequired(this.shadowRoot, ".suggested-prompts");
33743
- for (const prompt of appearance.suggestedPrompts) {
33744
- const button2 = document.createElement("button");
33745
- button2.type = "button";
33746
- button2.textContent = prompt;
33747
- button2.addEventListener("click", () => this.#sendGuarded(prompt));
33748
- prompts.append(button2);
33749
- }
34064
+ this.#suggestions.render();
33750
34065
  if (!appearance.behavior.showLauncher)
33751
34066
  queryRequired(this.shadowRoot, ".launcher").hidden = true;
33752
34067
  if (!appearance.behavior.showHeader)