@noodleseed/assistant 1.27.0 → 1.29.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 (41) 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-3VZ3M5BE.js} +92 -9
  6. package/dist/{chunk-WJV4MJUR.js.map → chunk-3VZ3M5BE.js.map} +1 -1
  7. package/dist/{chunk-2A3ZEYZV.js → chunk-DOWFW3KA.js} +260 -55
  8. package/dist/chunk-DOWFW3KA.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 +91 -8
  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 +47 -47
  17. package/dist/index.cjs +349 -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 +91 -8
  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 +349 -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-2A3ZEYZV.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;
@@ -27908,6 +27933,7 @@ var DefaultAssistantClient = class {
27908
27933
  #context;
27909
27934
  #modelContext;
27910
27935
  #active;
27936
+ #initialSuggestions;
27911
27937
  #pendingAppInteractions = /* @__PURE__ */ new Map();
27912
27938
  constructor(options) {
27913
27939
  this.#source = resolveSessionSource(options);
@@ -27969,10 +27995,13 @@ var DefaultAssistantClient = class {
27969
27995
  "turn_failed"
27970
27996
  );
27971
27997
  if (!response.ok) return;
27972
- for (const event of transcriptReplayEvents(parseTranscriptEntries(await response.json()))) {
27998
+ const value = await response.json();
27999
+ for (const event of transcriptReplayEvents(parseTranscriptEntries(value))) {
27973
28000
  this.#emit(event);
27974
28001
  if (event.event === "message_completed") await this.#chat.flush();
27975
28002
  }
28003
+ const suggestions = parseTranscriptSuggestions(value);
28004
+ if (suggestions) this.#emit({ event: "suggested_prompts", data: suggestions });
27976
28005
  }
27977
28006
  /** POST the one-shot resume trigger; a 409 means nothing was pending, which renders as silence. */
27978
28007
  async #resumeTurn(session, signal) {
@@ -27981,7 +28010,10 @@ var DefaultAssistantClient = class {
27981
28010
  {
27982
28011
  method: "POST",
27983
28012
  headers: authorizedHeaders(session.token, "text/event-stream"),
27984
- body: JSON.stringify({ resume: true }),
28013
+ body: JSON.stringify({
28014
+ resume: true,
28015
+ ...session.endpoints.suggestions ? { suggestions: true } : {}
28016
+ }),
27985
28017
  signal
27986
28018
  },
27987
28019
  "turn_failed"
@@ -28004,6 +28036,7 @@ var DefaultAssistantClient = class {
28004
28036
  async sendMessage(text3) {
28005
28037
  const message = text3.trim();
28006
28038
  if (!message) return;
28039
+ this.#cancelInitialSuggestions();
28007
28040
  await this.#singleFlight(async (signal) => {
28008
28041
  await this.#runChatOperation(
28009
28042
  signal,
@@ -28011,10 +28044,42 @@ var DefaultAssistantClient = class {
28011
28044
  );
28012
28045
  });
28013
28046
  }
28047
+ async loadInitialSuggestions() {
28048
+ if (!this.#session) await this.connect();
28049
+ const session = this.#session;
28050
+ if (!session?.endpoints.suggestions) return;
28051
+ if (this.#initialSuggestions) return this.#initialSuggestions.promise;
28052
+ const controller = new AbortController();
28053
+ const clientContext = this.#resolveClientContext();
28054
+ const pageContext = this.#resolvePageContext();
28055
+ const promise2 = this.#request(
28056
+ session.endpoints.suggestions,
28057
+ {
28058
+ method: "POST",
28059
+ headers: authorizedHeaders(session.token, "text/event-stream"),
28060
+ body: JSON.stringify({
28061
+ ...clientContext ? { clientContext } : {},
28062
+ ...pageContext === void 0 ? {} : { pageContext },
28063
+ ...this.#modelContext ? { modelContext: this.#modelContext } : {}
28064
+ }),
28065
+ signal: controller.signal
28066
+ },
28067
+ "turn_failed"
28068
+ ).then(async (response) => {
28069
+ if (response.ok) await this.#consume(response);
28070
+ }).finally(() => {
28071
+ if (this.#initialSuggestions?.controller === controller) {
28072
+ this.#initialSuggestions = void 0;
28073
+ }
28074
+ });
28075
+ this.#initialSuggestions = { controller, promise: promise2 };
28076
+ return promise2;
28077
+ }
28014
28078
  async respond(id, resolution) {
28015
28079
  if (!id) {
28016
28080
  throw clientError("invalid_request", "interaction id is required", false);
28017
28081
  }
28082
+ this.#cancelInitialSuggestions();
28018
28083
  await this.#singleFlight(async (signal) => {
28019
28084
  await this.#runChatOperation(signal, async () => {
28020
28085
  const pendingApp = this.#pendingAppInteractions.get(id);
@@ -28034,6 +28099,7 @@ var DefaultAssistantClient = class {
28034
28099
  const body = modernEndpoint ? {
28035
28100
  id,
28036
28101
  action: resolution.action,
28102
+ ...session.endpoints.suggestions ? { suggestions: true } : {},
28037
28103
  ...resolution.action === "accept" && resolution.content !== void 0 ? { content: resolution.content } : {}
28038
28104
  } : { id };
28039
28105
  this.#emit({ event: "interaction_started", data: { id, action: resolution.action } });
@@ -28112,7 +28178,7 @@ var DefaultAssistantClient = class {
28112
28178
  this.#modelContext = copyAssistantModelContext(update);
28113
28179
  this.#emit({ event: "model_context_changed", data: { modelContext: this.#modelContext } });
28114
28180
  }
28115
- async requestApp(method, params) {
28181
+ async requestApp(method, params, options) {
28116
28182
  const session = this.#session;
28117
28183
  if (!session?.endpoints.apps) {
28118
28184
  throw clientError(
@@ -28126,7 +28192,11 @@ var DefaultAssistantClient = class {
28126
28192
  {
28127
28193
  method: "POST",
28128
28194
  headers: authorizedHeaders(session.token, "application/json"),
28129
- body: JSON.stringify({ method, params })
28195
+ body: JSON.stringify({
28196
+ ...options?.bridge ? { bridge: options.bridge } : {},
28197
+ method,
28198
+ params
28199
+ })
28130
28200
  },
28131
28201
  "app_request_failed"
28132
28202
  );
@@ -28141,6 +28211,7 @@ var DefaultAssistantClient = class {
28141
28211
  const value = await response.json();
28142
28212
  const interaction = parseAppInteraction(value);
28143
28213
  if (!interaction) return value;
28214
+ options?.onSuspended?.();
28144
28215
  return new Promise((resolve2, reject) => {
28145
28216
  const id = interaction.data.id;
28146
28217
  const replaced = this.#pendingAppInteractions.get(id);
@@ -28158,6 +28229,7 @@ var DefaultAssistantClient = class {
28158
28229
  return this.#session?.endpoints.sandbox;
28159
28230
  }
28160
28231
  resetSession() {
28232
+ this.#cancelInitialSuggestions();
28161
28233
  this.#rejectPendingAppInteractions(
28162
28234
  clientError("confirmation_expired", "assistant session is not active", false)
28163
28235
  );
@@ -28166,6 +28238,7 @@ var DefaultAssistantClient = class {
28166
28238
  }
28167
28239
  abort() {
28168
28240
  this.#active?.abort();
28241
+ this.#cancelInitialSuggestions();
28169
28242
  }
28170
28243
  async #sendTurn(message, mayRetry, signal, modelContext) {
28171
28244
  const session = this.#session ?? await this.#createSession(signal);
@@ -28181,7 +28254,8 @@ var DefaultAssistantClient = class {
28181
28254
  message,
28182
28255
  ...clientContext ? { clientContext } : {},
28183
28256
  ...pageContext === void 0 ? {} : { pageContext },
28184
- ...modelContext ? { modelContext } : {}
28257
+ ...modelContext ? { modelContext } : {},
28258
+ ...session.endpoints.suggestions ? { suggestions: true } : {}
28185
28259
  }),
28186
28260
  signal
28187
28261
  },
@@ -28370,6 +28444,9 @@ var DefaultAssistantClient = class {
28370
28444
  this.#active = void 0;
28371
28445
  }
28372
28446
  }
28447
+ #cancelInitialSuggestions() {
28448
+ this.#initialSuggestions?.controller.abort();
28449
+ }
28373
28450
  async #runChatOperation(signal, operation) {
28374
28451
  try {
28375
28452
  await operation();
@@ -28450,6 +28527,22 @@ function appendConversationRecovery(input) {
28450
28527
  input.revealLatest();
28451
28528
  }
28452
28529
 
28530
+ // src/element-error-recovery.ts
28531
+ function renderConversationError(options) {
28532
+ const { appearance, error: error51 } = options;
28533
+ const detail = error51 instanceof AssistantClientError ? error51.detail : { code: options.fallbackCode, retryable: false };
28534
+ const startNewConversation = detail.serviceCode === "session_turn_budget_exhausted";
28535
+ const reconnect = detail.retryable !== false || detail.serviceCode === void 0;
28536
+ const recoveryAction = startNewConversation ? { label: appearance.labels.newConversation, run: () => options.startNewConversation() } : reconnect ? { label: appearance.labels.reconnect, run: () => options.reconnect() } : void 0;
28537
+ appendConversationRecovery({
28538
+ messages: options.messages,
28539
+ message: detail.code === "session_expired" ? appearance.labels.sessionExpired : appearance.labels.unavailable,
28540
+ ...recoveryAction === void 0 ? {} : { action: recoveryAction },
28541
+ revealLatest: () => options.revealLatest()
28542
+ });
28543
+ return detail;
28544
+ }
28545
+
28453
28546
  // src/input-request-card.ts
28454
28547
  function createInputRequestCard(options) {
28455
28548
  const card = document.createElement("section");
@@ -32474,7 +32567,7 @@ var ASSISTANT_ELEMENT_STYLES = `<style>
32474
32567
  .launcher[hidden] { display: none; }
32475
32568
  .launcher:hover { transform: scale(1.02); box-shadow: var(--ns-assistant-halo-launcher-shadow-hover), -6px 0 18px color-mix(in srgb, var(--ns-assistant-accent, var(--ns-assistant-default-accent)) 20%, transparent), 6px 0 18px color-mix(in srgb, var(--ns-assistant-accent, var(--ns-assistant-default-accent)) 20%, transparent); }
32476
32569
  .launcher-trigger, .launcher-form { min-width: 0; margin: 0; padding: 0; border: 0; background: transparent; font: inherit; }
32477
- .launcher-trigger { display: flex; align-items: center; gap: 12px; min-height: 28px; color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); cursor: text; }
32570
+ .launcher-trigger { display: flex; align-items: center; gap: 12px; min-height: 28px; color: var(--ns-assistant-launcher-text, var(--ns-assistant-text, var(--ns-assistant-default-text))); cursor: text; }
32478
32571
  .launcher-label { padding-left: 6px; white-space: nowrap; font-size: 15px; line-height: 20px; }
32479
32572
  .launcher-arrow, .launcher-send { position: relative; display: grid; width: 28px; height: 28px; flex: 0 0 28px; place-items: center; border: 0; border-radius: 50%; color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: var(--ns-assistant-launcher-action, var(--ns-assistant-halo-launcher-action)); cursor: pointer; }
32480
32573
  .launcher-arrow:hover, .launcher-send:hover { background: var(--ns-assistant-launcher-action-hover, var(--ns-assistant-halo-launcher-action-hover)); }
@@ -32623,6 +32716,59 @@ var ASSISTANT_ELEMENT_STYLES = `<style>
32623
32716
  @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
32717
  </style>`;
32625
32718
 
32719
+ // src/element-suggestions-controller.ts
32720
+ var AssistantElementSuggestionsController = class {
32721
+ #host;
32722
+ #initialRequested = false;
32723
+ #dynamic;
32724
+ constructor(host) {
32725
+ this.#host = host;
32726
+ }
32727
+ reset() {
32728
+ this.#initialRequested = false;
32729
+ this.clear();
32730
+ }
32731
+ clear() {
32732
+ this.#dynamic = void 0;
32733
+ this.render();
32734
+ }
32735
+ handle(event) {
32736
+ if (event.event === "message_started" || event.event === "interaction_started") {
32737
+ this.clear();
32738
+ return;
32739
+ }
32740
+ if (event.event !== "suggested_prompts") return;
32741
+ this.#dynamic = event.data;
32742
+ this.render();
32743
+ }
32744
+ maybeLoadInitial() {
32745
+ const appearance = this.#host.appearance();
32746
+ if (this.#initialRequested || appearance.suggestedPromptsSource !== "model" || this.#host.hasMessages() || !this.#host.isOpen()) {
32747
+ return;
32748
+ }
32749
+ const client = this.#host.client();
32750
+ if (!client?.loadInitialSuggestions) return;
32751
+ this.#initialRequested = true;
32752
+ void client.loadInitialSuggestions().catch(() => {
32753
+ });
32754
+ }
32755
+ render() {
32756
+ const container = this.#host.root()?.querySelector(".suggested-prompts");
32757
+ if (!container) return;
32758
+ const appearance = this.#host.appearance();
32759
+ const hasMessages = this.#host.hasMessages();
32760
+ const prompts = (this.#dynamic?.phase === "follow_up" || !hasMessages ? this.#dynamic?.prompts : void 0) ?? (!hasMessages && appearance.suggestedPromptsSource === "configured" ? appearance.suggestedPrompts : []);
32761
+ container.replaceChildren();
32762
+ for (const prompt of prompts) {
32763
+ const button2 = document.createElement("button");
32764
+ button2.type = "button";
32765
+ button2.textContent = prompt;
32766
+ button2.addEventListener("click", () => this.#host.send(prompt));
32767
+ container.append(button2);
32768
+ }
32769
+ }
32770
+ };
32771
+
32626
32772
  // src/host-appearance.ts
32627
32773
  var ROLE_VARIABLES = {
32628
32774
  canvas: "--ns-assistant-canvas",
@@ -32931,6 +33077,147 @@ function applyAssistantElementTheme(input) {
32931
33077
  return warnings;
32932
33078
  }
32933
33079
 
33080
+ // src/webmcp-bridge.ts
33081
+ var INERT = { registeredToolNames: [], stop: () => {
33082
+ } };
33083
+ async function startWebMcpBridge(ports) {
33084
+ const { client, modelContext, enabled } = ports;
33085
+ if (!enabled || !modelContext) return INERT;
33086
+ let listed;
33087
+ try {
33088
+ listed = await client.requestApp("tools/list", {});
33089
+ } catch {
33090
+ return INERT;
33091
+ }
33092
+ const registrations = [];
33093
+ for (const tool of projectableTools(listed)) {
33094
+ registrations.push({
33095
+ name: tool.name,
33096
+ handle: modelContext.registerTool({
33097
+ name: tool.name,
33098
+ ...tool.description === void 0 ? {} : { description: tool.description },
33099
+ ...tool.inputSchema === void 0 ? {} : { inputSchema: tool.inputSchema },
33100
+ // Passed through, never inferred: an agent may use `readOnlyHint` to decide it can call
33101
+ // without asking, so a hint this bridge invented would be a hint nobody authored.
33102
+ ...tool.annotations === void 0 ? {} : { annotations: tool.annotations },
33103
+ execute: (args) => executeThroughAssistant(client, tool.name, args)
33104
+ })
33105
+ });
33106
+ }
33107
+ let stopped = false;
33108
+ return {
33109
+ get registeredToolNames() {
33110
+ return stopped ? [] : registrations.map((entry) => entry.name);
33111
+ },
33112
+ stop() {
33113
+ if (stopped) return;
33114
+ stopped = true;
33115
+ for (const entry of registrations) entry.handle?.unregister?.();
33116
+ }
33117
+ };
33118
+ }
33119
+ function projectableTools(listed) {
33120
+ const tools = isRecord7(listed) && Array.isArray(listed.tools) ? listed.tools : [];
33121
+ const projectable = [];
33122
+ for (const tool of tools) {
33123
+ if (!isRecord7(tool) || typeof tool.name !== "string" || tool.name.length === 0) continue;
33124
+ const visibility = readVisibility(tool);
33125
+ if (visibility !== void 0 && !(visibility.includes("app") && visibility.includes("model"))) {
33126
+ continue;
33127
+ }
33128
+ projectable.push({
33129
+ name: tool.name,
33130
+ description: typeof tool.description === "string" ? tool.description : void 0,
33131
+ inputSchema: isRecord7(tool.inputSchema) ? tool.inputSchema : void 0,
33132
+ annotations: isRecord7(tool.annotations) ? tool.annotations : void 0
33133
+ });
33134
+ }
33135
+ return projectable;
33136
+ }
33137
+ function readVisibility(tool) {
33138
+ const meta3 = isRecord7(tool._meta) ? tool._meta : void 0;
33139
+ const ui = meta3 && isRecord7(meta3.ui) ? meta3.ui : void 0;
33140
+ const visibility = ui?.visibility;
33141
+ return Array.isArray(visibility) ? visibility.filter((entry) => typeof entry === "string") : void 0;
33142
+ }
33143
+ async function executeThroughAssistant(client, name21, args) {
33144
+ let suspend;
33145
+ const suspended = new Promise((resolve2) => {
33146
+ suspend = resolve2;
33147
+ });
33148
+ try {
33149
+ const call = client.requestApp(
33150
+ "tools/call",
33151
+ { name: name21, arguments: args },
33152
+ // Attribution only: anyone holding the session token could send this, and it selects a
33153
+ // *narrower* budget than the unmarked path, so declaring it can only cost the declarer.
33154
+ { bridge: "webmcp", onSuspended: suspend }
33155
+ );
33156
+ void call.catch(() => {
33157
+ });
33158
+ const outcome = await Promise.race([
33159
+ call.then((value) => ({ kind: "result", value })),
33160
+ suspended.then(() => ({ kind: "confirmation" }))
33161
+ ]);
33162
+ return outcome.kind === "confirmation" ? CONFIRMATION_REQUIRED : toToolResult(outcome.value);
33163
+ } catch (error51) {
33164
+ return textResult(error51 instanceof Error ? error51.message : "tool call failed", true);
33165
+ }
33166
+ }
33167
+ var CONFIRMATION_REQUIRED = {
33168
+ content: [
33169
+ {
33170
+ type: "text",
33171
+ text: "confirmation required: open the assistant panel on this page to approve this action"
33172
+ }
33173
+ ],
33174
+ isError: true
33175
+ };
33176
+ function toToolResult(value) {
33177
+ if (isRecord7(value) && Array.isArray(value.content)) return value;
33178
+ return textResult(JSON.stringify(value ?? null), false);
33179
+ }
33180
+ function textResult(text3, isError) {
33181
+ return { content: [{ type: "text", text: text3 }], isError };
33182
+ }
33183
+ function isRecord7(value) {
33184
+ return typeof value === "object" && value !== null && !Array.isArray(value);
33185
+ }
33186
+
33187
+ // src/element-webmcp-controller.ts
33188
+ var AssistantElementWebMcpController = class {
33189
+ #enabled = false;
33190
+ #bridge;
33191
+ /** Guards against a late `startWebMcpBridge` resolving after the session it belonged to is gone. */
33192
+ #generation = 0;
33193
+ /** Called when resolved configuration arrives; the deployment's opt-in is the only source. */
33194
+ configure(enabled) {
33195
+ this.#enabled = enabled;
33196
+ if (!enabled) this.stop();
33197
+ }
33198
+ start(client) {
33199
+ if (!this.#enabled || !client) return;
33200
+ const modelContext = browserModelContext();
33201
+ if (!modelContext) return;
33202
+ this.stop();
33203
+ const generation = ++this.#generation;
33204
+ void startWebMcpBridge({ client, modelContext, enabled: true }).then((bridge) => {
33205
+ if (generation === this.#generation) this.#bridge = bridge;
33206
+ else bridge.stop();
33207
+ });
33208
+ }
33209
+ stop() {
33210
+ this.#generation += 1;
33211
+ this.#bridge?.stop();
33212
+ this.#bridge = void 0;
33213
+ }
33214
+ };
33215
+ function browserModelContext() {
33216
+ if (typeof document === "undefined") return void 0;
33217
+ const host = document;
33218
+ return typeof host.modelContext?.registerTool === "function" ? host.modelContext : void 0;
33219
+ }
33220
+
32934
33221
  // src/presentation-styles.ts
32935
33222
  var presentationStyles = `<style>
32936
33223
  :host([data-mode="floating"]) {
@@ -33174,9 +33461,18 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33174
33461
  #turnInFlight = false;
33175
33462
  #fetch;
33176
33463
  #hostAppearance;
33464
+ #webmcp = new AssistantElementWebMcpController();
33177
33465
  #appearanceWarningKeys = /* @__PURE__ */ new Set();
33178
33466
  #appearanceStyleSnapshot = /* @__PURE__ */ new Map();
33179
33467
  #scroll = new AssistantElementScrollController();
33468
+ #suggestions = new AssistantElementSuggestionsController({
33469
+ root: () => this.shadowRoot,
33470
+ appearance: () => this.#appearance,
33471
+ client: () => this.#client,
33472
+ hasMessages: () => this.hasAttribute("has-messages"),
33473
+ isOpen: () => this.hasAttribute("open"),
33474
+ send: (prompt) => this.#sendGuarded(prompt)
33475
+ });
33180
33476
  sessionEndpoint = "";
33181
33477
  /** Public mount: the non-secret embed id `noodle deploy` printed. Mutually exclusive with the above. */
33182
33478
  embedId = "";
@@ -33288,6 +33584,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33288
33584
  this.#launcher.disconnect();
33289
33585
  this.#setBusy(false);
33290
33586
  this.#environment.disconnect();
33587
+ this.#webmcp.stop();
33291
33588
  }
33292
33589
  attributeChangedCallback(name21) {
33293
33590
  if (name21 === "theme") this.#applyTheme();
@@ -33321,6 +33618,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33321
33618
  }
33322
33619
  resetSession() {
33323
33620
  this.#conversationGeneration += 1;
33621
+ this.#webmcp.stop();
33324
33622
  this.#client?.abort();
33325
33623
  this.#unsubscribeClient?.();
33326
33624
  this.#client = void 0;
@@ -33330,12 +33628,14 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33330
33628
  this.#events.resetConversation();
33331
33629
  this.#pendingUserEcho = void 0;
33332
33630
  this.removeAttribute("has-messages");
33631
+ this.#suggestions.reset();
33333
33632
  this.#setBusy(false);
33334
33633
  this.#setSessionState("idle");
33335
33634
  }
33336
33635
  /** Re-establish the session without replaying a message or interaction decision. */
33337
33636
  reconnect() {
33338
33637
  this.#conversationGeneration += 1;
33638
+ this.#webmcp.stop();
33339
33639
  this.#client?.abort();
33340
33640
  this.#unsubscribeClient?.();
33341
33641
  this.#client = void 0;
@@ -33393,6 +33693,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33393
33693
  this.#turnInFlight = true;
33394
33694
  this.#pendingUserEcho = message;
33395
33695
  this.#appendMessage("user", message);
33696
+ this.#suggestions.clear();
33396
33697
  this.#setBusy(true);
33397
33698
  if (this.#sessionState !== "ready") this.#setSessionState("loading");
33398
33699
  try {
@@ -33412,12 +33713,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33412
33713
  if (generation === this.#conversationGeneration) this.#setBusy(false);
33413
33714
  }
33414
33715
  }
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
- */
33716
+ /** DOM listeners swallow after the failing path dispatches its typed assistant-error event. */
33421
33717
  #sendGuarded(text3) {
33422
33718
  void this.sendMessage(text3).catch(() => {
33423
33719
  });
@@ -33469,13 +33765,21 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33469
33765
  }
33470
33766
  #primeSession() {
33471
33767
  if (!this.sessionEndpoint && !this.embedId) return;
33472
- if (this.#sessionBootstrap || this.#client?.hasSession()) return;
33768
+ if (this.#sessionBootstrap) return;
33769
+ if (this.#client?.hasSession()) {
33770
+ this.#suggestions.maybeLoadInitial();
33771
+ return;
33772
+ }
33473
33773
  if (this.#client?.isBusy?.() === true) return;
33474
33774
  if (this.embedId && !this.#pageContext) void this.#automaticPageContext.refresh();
33475
33775
  const client = this.#ensureClient();
33476
33776
  const generation = this.#conversationGeneration;
33477
33777
  this.#setSessionState("loading");
33478
- const bootstrap = client.connect().catch((error51) => {
33778
+ const bootstrap = client.connect().then(() => {
33779
+ if (generation === this.#conversationGeneration && this.#client === client) {
33780
+ this.#suggestions.maybeLoadInitial();
33781
+ }
33782
+ }).catch((error51) => {
33479
33783
  if (generation !== this.#conversationGeneration || this.#client !== client) return;
33480
33784
  this.#dispatchClientError(error51, "session_failed");
33481
33785
  this.#presentationGate.reveal();
@@ -33487,15 +33791,18 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33487
33791
  #handleClientEvent(event) {
33488
33792
  this.dispatchEvent(new CustomEvent("assistant-event", { detail: event }));
33489
33793
  this.#events.handle(event);
33794
+ this.#suggestions.handle(event);
33490
33795
  if (event.event === "session_started") {
33491
33796
  this.#publicConfiguration.disconnect();
33492
33797
  this.removeAttribute("data-public-configuration-loading");
33493
33798
  this.#presentationGate.reveal();
33494
33799
  this.#setSessionState("ready");
33495
33800
  this.#messages?.querySelector(".conversation-error")?.remove();
33801
+ this.#webmcp.start(this.#client);
33496
33802
  }
33497
33803
  if (event.event === "session_expired" || event.event === "session_reset") {
33498
33804
  this.#setSessionState("idle");
33805
+ this.#webmcp.stop();
33499
33806
  }
33500
33807
  if (event.event === "tool_proposed" || event.event === "interaction_proposed" || event.event === "input_requested") {
33501
33808
  this.#removeThinking();
@@ -33599,23 +33906,17 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33599
33906
  if (error51 instanceof AssistantClientError && error51.detail.code === "session_failed") {
33600
33907
  this.#setSessionState("error");
33601
33908
  }
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);
33909
+ this.#dispatchError(
33910
+ renderConversationError({
33911
+ messages: this.#messages,
33912
+ appearance: this.#appearance,
33913
+ error: error51,
33914
+ fallbackCode,
33915
+ startNewConversation: () => this.#startNewConversation(),
33916
+ reconnect: () => this.reconnect(),
33917
+ revealLatest: () => this.#revealLatest()
33918
+ })
33919
+ );
33619
33920
  }
33620
33921
  #startNewConversation() {
33621
33922
  this.resetSession();
@@ -33648,7 +33949,9 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33648
33949
  }
33649
33950
  }
33650
33951
  #applyConfiguration(configuration) {
33952
+ this.#webmcp.configure(configuration.assistant?.webmcp?.enabled === true);
33651
33953
  this.#appearance = resolveAppearance(configuration);
33954
+ this.#suggestions.render();
33652
33955
  this.#syncAppearance();
33653
33956
  this.#applyStartOpenPolicy();
33654
33957
  }
@@ -33693,15 +33996,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33693
33996
  "aria-label",
33694
33997
  appearance.labels.close
33695
33998
  );
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
- }
33999
+ this.#suggestions.render();
33705
34000
  queryRequired(this.shadowRoot, "header").hidden = !appearance.behavior.showHeader;
33706
34001
  const legal = queryRequired(this.shadowRoot, ".legal");
33707
34002
  legal.replaceChildren();
@@ -33739,14 +34034,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33739
34034
  textarea.placeholder = appearance.labels.composerPlaceholder;
33740
34035
  textarea.setAttribute("aria-label", appearance.labels.composerPlaceholder);
33741
34036
  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
- }
34037
+ this.#suggestions.render();
33750
34038
  if (!appearance.behavior.showLauncher)
33751
34039
  queryRequired(this.shadowRoot, ".launcher").hidden = true;
33752
34040
  if (!appearance.behavior.showHeader)