@noodleseed/assistant 1.28.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-QH7PL3GU.js → chunk-DOWFW3KA.js} +259 -54
  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 +46 -46
  17. package/dist/index.cjs +348 -60
  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 +348 -60
  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
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;
@@ -27911,6 +27936,7 @@ var DefaultAssistantClient = class {
27911
27936
  #context;
27912
27937
  #modelContext;
27913
27938
  #active;
27939
+ #initialSuggestions;
27914
27940
  #pendingAppInteractions = /* @__PURE__ */ new Map();
27915
27941
  constructor(options) {
27916
27942
  this.#source = resolveSessionSource(options);
@@ -27972,10 +27998,13 @@ var DefaultAssistantClient = class {
27972
27998
  "turn_failed"
27973
27999
  );
27974
28000
  if (!response.ok) return;
27975
- for (const event of transcriptReplayEvents(parseTranscriptEntries(await response.json()))) {
28001
+ const value = await response.json();
28002
+ for (const event of transcriptReplayEvents(parseTranscriptEntries(value))) {
27976
28003
  this.#emit(event);
27977
28004
  if (event.event === "message_completed") await this.#chat.flush();
27978
28005
  }
28006
+ const suggestions = parseTranscriptSuggestions(value);
28007
+ if (suggestions) this.#emit({ event: "suggested_prompts", data: suggestions });
27979
28008
  }
27980
28009
  /** POST the one-shot resume trigger; a 409 means nothing was pending, which renders as silence. */
27981
28010
  async #resumeTurn(session, signal) {
@@ -27984,7 +28013,10 @@ var DefaultAssistantClient = class {
27984
28013
  {
27985
28014
  method: "POST",
27986
28015
  headers: authorizedHeaders(session.token, "text/event-stream"),
27987
- body: JSON.stringify({ resume: true }),
28016
+ body: JSON.stringify({
28017
+ resume: true,
28018
+ ...session.endpoints.suggestions ? { suggestions: true } : {}
28019
+ }),
27988
28020
  signal
27989
28021
  },
27990
28022
  "turn_failed"
@@ -28007,6 +28039,7 @@ var DefaultAssistantClient = class {
28007
28039
  async sendMessage(text3) {
28008
28040
  const message = text3.trim();
28009
28041
  if (!message) return;
28042
+ this.#cancelInitialSuggestions();
28010
28043
  await this.#singleFlight(async (signal) => {
28011
28044
  await this.#runChatOperation(
28012
28045
  signal,
@@ -28014,10 +28047,42 @@ var DefaultAssistantClient = class {
28014
28047
  );
28015
28048
  });
28016
28049
  }
28050
+ async loadInitialSuggestions() {
28051
+ if (!this.#session) await this.connect();
28052
+ const session = this.#session;
28053
+ if (!session?.endpoints.suggestions) return;
28054
+ if (this.#initialSuggestions) return this.#initialSuggestions.promise;
28055
+ const controller = new AbortController();
28056
+ const clientContext = this.#resolveClientContext();
28057
+ const pageContext = this.#resolvePageContext();
28058
+ const promise2 = this.#request(
28059
+ session.endpoints.suggestions,
28060
+ {
28061
+ method: "POST",
28062
+ headers: authorizedHeaders(session.token, "text/event-stream"),
28063
+ body: JSON.stringify({
28064
+ ...clientContext ? { clientContext } : {},
28065
+ ...pageContext === void 0 ? {} : { pageContext },
28066
+ ...this.#modelContext ? { modelContext: this.#modelContext } : {}
28067
+ }),
28068
+ signal: controller.signal
28069
+ },
28070
+ "turn_failed"
28071
+ ).then(async (response) => {
28072
+ if (response.ok) await this.#consume(response);
28073
+ }).finally(() => {
28074
+ if (this.#initialSuggestions?.controller === controller) {
28075
+ this.#initialSuggestions = void 0;
28076
+ }
28077
+ });
28078
+ this.#initialSuggestions = { controller, promise: promise2 };
28079
+ return promise2;
28080
+ }
28017
28081
  async respond(id, resolution) {
28018
28082
  if (!id) {
28019
28083
  throw clientError("invalid_request", "interaction id is required", false);
28020
28084
  }
28085
+ this.#cancelInitialSuggestions();
28021
28086
  await this.#singleFlight(async (signal) => {
28022
28087
  await this.#runChatOperation(signal, async () => {
28023
28088
  const pendingApp = this.#pendingAppInteractions.get(id);
@@ -28037,6 +28102,7 @@ var DefaultAssistantClient = class {
28037
28102
  const body = modernEndpoint ? {
28038
28103
  id,
28039
28104
  action: resolution.action,
28105
+ ...session.endpoints.suggestions ? { suggestions: true } : {},
28040
28106
  ...resolution.action === "accept" && resolution.content !== void 0 ? { content: resolution.content } : {}
28041
28107
  } : { id };
28042
28108
  this.#emit({ event: "interaction_started", data: { id, action: resolution.action } });
@@ -28115,7 +28181,7 @@ var DefaultAssistantClient = class {
28115
28181
  this.#modelContext = copyAssistantModelContext(update);
28116
28182
  this.#emit({ event: "model_context_changed", data: { modelContext: this.#modelContext } });
28117
28183
  }
28118
- async requestApp(method, params) {
28184
+ async requestApp(method, params, options) {
28119
28185
  const session = this.#session;
28120
28186
  if (!session?.endpoints.apps) {
28121
28187
  throw clientError(
@@ -28129,7 +28195,11 @@ var DefaultAssistantClient = class {
28129
28195
  {
28130
28196
  method: "POST",
28131
28197
  headers: authorizedHeaders(session.token, "application/json"),
28132
- body: JSON.stringify({ method, params })
28198
+ body: JSON.stringify({
28199
+ ...options?.bridge ? { bridge: options.bridge } : {},
28200
+ method,
28201
+ params
28202
+ })
28133
28203
  },
28134
28204
  "app_request_failed"
28135
28205
  );
@@ -28144,6 +28214,7 @@ var DefaultAssistantClient = class {
28144
28214
  const value = await response.json();
28145
28215
  const interaction = parseAppInteraction(value);
28146
28216
  if (!interaction) return value;
28217
+ options?.onSuspended?.();
28147
28218
  return new Promise((resolve2, reject) => {
28148
28219
  const id = interaction.data.id;
28149
28220
  const replaced = this.#pendingAppInteractions.get(id);
@@ -28161,6 +28232,7 @@ var DefaultAssistantClient = class {
28161
28232
  return this.#session?.endpoints.sandbox;
28162
28233
  }
28163
28234
  resetSession() {
28235
+ this.#cancelInitialSuggestions();
28164
28236
  this.#rejectPendingAppInteractions(
28165
28237
  clientError("confirmation_expired", "assistant session is not active", false)
28166
28238
  );
@@ -28169,6 +28241,7 @@ var DefaultAssistantClient = class {
28169
28241
  }
28170
28242
  abort() {
28171
28243
  this.#active?.abort();
28244
+ this.#cancelInitialSuggestions();
28172
28245
  }
28173
28246
  async #sendTurn(message, mayRetry, signal, modelContext) {
28174
28247
  const session = this.#session ?? await this.#createSession(signal);
@@ -28184,7 +28257,8 @@ var DefaultAssistantClient = class {
28184
28257
  message,
28185
28258
  ...clientContext ? { clientContext } : {},
28186
28259
  ...pageContext === void 0 ? {} : { pageContext },
28187
- ...modelContext ? { modelContext } : {}
28260
+ ...modelContext ? { modelContext } : {},
28261
+ ...session.endpoints.suggestions ? { suggestions: true } : {}
28188
28262
  }),
28189
28263
  signal
28190
28264
  },
@@ -28373,6 +28447,9 @@ var DefaultAssistantClient = class {
28373
28447
  this.#active = void 0;
28374
28448
  }
28375
28449
  }
28450
+ #cancelInitialSuggestions() {
28451
+ this.#initialSuggestions?.controller.abort();
28452
+ }
28376
28453
  async #runChatOperation(signal, operation) {
28377
28454
  try {
28378
28455
  await operation();
@@ -28453,6 +28530,22 @@ function appendConversationRecovery(input) {
28453
28530
  input.revealLatest();
28454
28531
  }
28455
28532
 
28533
+ // src/element-error-recovery.ts
28534
+ function renderConversationError(options) {
28535
+ const { appearance, error: error51 } = options;
28536
+ const detail = error51 instanceof AssistantClientError ? error51.detail : { code: options.fallbackCode, retryable: false };
28537
+ const startNewConversation = detail.serviceCode === "session_turn_budget_exhausted";
28538
+ const reconnect = detail.retryable !== false || detail.serviceCode === void 0;
28539
+ const recoveryAction = startNewConversation ? { label: appearance.labels.newConversation, run: () => options.startNewConversation() } : reconnect ? { label: appearance.labels.reconnect, run: () => options.reconnect() } : void 0;
28540
+ appendConversationRecovery({
28541
+ messages: options.messages,
28542
+ message: detail.code === "session_expired" ? appearance.labels.sessionExpired : appearance.labels.unavailable,
28543
+ ...recoveryAction === void 0 ? {} : { action: recoveryAction },
28544
+ revealLatest: () => options.revealLatest()
28545
+ });
28546
+ return detail;
28547
+ }
28548
+
28456
28549
  // src/input-request-card.ts
28457
28550
  function createInputRequestCard(options) {
28458
28551
  const card = document.createElement("section");
@@ -32626,6 +32719,59 @@ var ASSISTANT_ELEMENT_STYLES = `<style>
32626
32719
  @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
32720
  </style>`;
32628
32721
 
32722
+ // src/element-suggestions-controller.ts
32723
+ var AssistantElementSuggestionsController = class {
32724
+ #host;
32725
+ #initialRequested = false;
32726
+ #dynamic;
32727
+ constructor(host) {
32728
+ this.#host = host;
32729
+ }
32730
+ reset() {
32731
+ this.#initialRequested = false;
32732
+ this.clear();
32733
+ }
32734
+ clear() {
32735
+ this.#dynamic = void 0;
32736
+ this.render();
32737
+ }
32738
+ handle(event) {
32739
+ if (event.event === "message_started" || event.event === "interaction_started") {
32740
+ this.clear();
32741
+ return;
32742
+ }
32743
+ if (event.event !== "suggested_prompts") return;
32744
+ this.#dynamic = event.data;
32745
+ this.render();
32746
+ }
32747
+ maybeLoadInitial() {
32748
+ const appearance = this.#host.appearance();
32749
+ if (this.#initialRequested || appearance.suggestedPromptsSource !== "model" || this.#host.hasMessages() || !this.#host.isOpen()) {
32750
+ return;
32751
+ }
32752
+ const client = this.#host.client();
32753
+ if (!client?.loadInitialSuggestions) return;
32754
+ this.#initialRequested = true;
32755
+ void client.loadInitialSuggestions().catch(() => {
32756
+ });
32757
+ }
32758
+ render() {
32759
+ const container = this.#host.root()?.querySelector(".suggested-prompts");
32760
+ if (!container) return;
32761
+ const appearance = this.#host.appearance();
32762
+ const hasMessages = this.#host.hasMessages();
32763
+ const prompts = (this.#dynamic?.phase === "follow_up" || !hasMessages ? this.#dynamic?.prompts : void 0) ?? (!hasMessages && appearance.suggestedPromptsSource === "configured" ? appearance.suggestedPrompts : []);
32764
+ container.replaceChildren();
32765
+ for (const prompt of prompts) {
32766
+ const button2 = document.createElement("button");
32767
+ button2.type = "button";
32768
+ button2.textContent = prompt;
32769
+ button2.addEventListener("click", () => this.#host.send(prompt));
32770
+ container.append(button2);
32771
+ }
32772
+ }
32773
+ };
32774
+
32629
32775
  // src/host-appearance.ts
32630
32776
  var ROLE_VARIABLES = {
32631
32777
  canvas: "--ns-assistant-canvas",
@@ -32934,6 +33080,147 @@ function applyAssistantElementTheme(input) {
32934
33080
  return warnings;
32935
33081
  }
32936
33082
 
33083
+ // src/webmcp-bridge.ts
33084
+ var INERT = { registeredToolNames: [], stop: () => {
33085
+ } };
33086
+ async function startWebMcpBridge(ports) {
33087
+ const { client, modelContext, enabled } = ports;
33088
+ if (!enabled || !modelContext) return INERT;
33089
+ let listed;
33090
+ try {
33091
+ listed = await client.requestApp("tools/list", {});
33092
+ } catch {
33093
+ return INERT;
33094
+ }
33095
+ const registrations = [];
33096
+ for (const tool of projectableTools(listed)) {
33097
+ registrations.push({
33098
+ name: tool.name,
33099
+ handle: modelContext.registerTool({
33100
+ name: tool.name,
33101
+ ...tool.description === void 0 ? {} : { description: tool.description },
33102
+ ...tool.inputSchema === void 0 ? {} : { inputSchema: tool.inputSchema },
33103
+ // Passed through, never inferred: an agent may use `readOnlyHint` to decide it can call
33104
+ // without asking, so a hint this bridge invented would be a hint nobody authored.
33105
+ ...tool.annotations === void 0 ? {} : { annotations: tool.annotations },
33106
+ execute: (args) => executeThroughAssistant(client, tool.name, args)
33107
+ })
33108
+ });
33109
+ }
33110
+ let stopped = false;
33111
+ return {
33112
+ get registeredToolNames() {
33113
+ return stopped ? [] : registrations.map((entry) => entry.name);
33114
+ },
33115
+ stop() {
33116
+ if (stopped) return;
33117
+ stopped = true;
33118
+ for (const entry of registrations) entry.handle?.unregister?.();
33119
+ }
33120
+ };
33121
+ }
33122
+ function projectableTools(listed) {
33123
+ const tools = isRecord7(listed) && Array.isArray(listed.tools) ? listed.tools : [];
33124
+ const projectable = [];
33125
+ for (const tool of tools) {
33126
+ if (!isRecord7(tool) || typeof tool.name !== "string" || tool.name.length === 0) continue;
33127
+ const visibility = readVisibility(tool);
33128
+ if (visibility !== void 0 && !(visibility.includes("app") && visibility.includes("model"))) {
33129
+ continue;
33130
+ }
33131
+ projectable.push({
33132
+ name: tool.name,
33133
+ description: typeof tool.description === "string" ? tool.description : void 0,
33134
+ inputSchema: isRecord7(tool.inputSchema) ? tool.inputSchema : void 0,
33135
+ annotations: isRecord7(tool.annotations) ? tool.annotations : void 0
33136
+ });
33137
+ }
33138
+ return projectable;
33139
+ }
33140
+ function readVisibility(tool) {
33141
+ const meta3 = isRecord7(tool._meta) ? tool._meta : void 0;
33142
+ const ui = meta3 && isRecord7(meta3.ui) ? meta3.ui : void 0;
33143
+ const visibility = ui?.visibility;
33144
+ return Array.isArray(visibility) ? visibility.filter((entry) => typeof entry === "string") : void 0;
33145
+ }
33146
+ async function executeThroughAssistant(client, name21, args) {
33147
+ let suspend;
33148
+ const suspended = new Promise((resolve2) => {
33149
+ suspend = resolve2;
33150
+ });
33151
+ try {
33152
+ const call = client.requestApp(
33153
+ "tools/call",
33154
+ { name: name21, arguments: args },
33155
+ // Attribution only: anyone holding the session token could send this, and it selects a
33156
+ // *narrower* budget than the unmarked path, so declaring it can only cost the declarer.
33157
+ { bridge: "webmcp", onSuspended: suspend }
33158
+ );
33159
+ void call.catch(() => {
33160
+ });
33161
+ const outcome = await Promise.race([
33162
+ call.then((value) => ({ kind: "result", value })),
33163
+ suspended.then(() => ({ kind: "confirmation" }))
33164
+ ]);
33165
+ return outcome.kind === "confirmation" ? CONFIRMATION_REQUIRED : toToolResult(outcome.value);
33166
+ } catch (error51) {
33167
+ return textResult(error51 instanceof Error ? error51.message : "tool call failed", true);
33168
+ }
33169
+ }
33170
+ var CONFIRMATION_REQUIRED = {
33171
+ content: [
33172
+ {
33173
+ type: "text",
33174
+ text: "confirmation required: open the assistant panel on this page to approve this action"
33175
+ }
33176
+ ],
33177
+ isError: true
33178
+ };
33179
+ function toToolResult(value) {
33180
+ if (isRecord7(value) && Array.isArray(value.content)) return value;
33181
+ return textResult(JSON.stringify(value ?? null), false);
33182
+ }
33183
+ function textResult(text3, isError) {
33184
+ return { content: [{ type: "text", text: text3 }], isError };
33185
+ }
33186
+ function isRecord7(value) {
33187
+ return typeof value === "object" && value !== null && !Array.isArray(value);
33188
+ }
33189
+
33190
+ // src/element-webmcp-controller.ts
33191
+ var AssistantElementWebMcpController = class {
33192
+ #enabled = false;
33193
+ #bridge;
33194
+ /** Guards against a late `startWebMcpBridge` resolving after the session it belonged to is gone. */
33195
+ #generation = 0;
33196
+ /** Called when resolved configuration arrives; the deployment's opt-in is the only source. */
33197
+ configure(enabled) {
33198
+ this.#enabled = enabled;
33199
+ if (!enabled) this.stop();
33200
+ }
33201
+ start(client) {
33202
+ if (!this.#enabled || !client) return;
33203
+ const modelContext = browserModelContext();
33204
+ if (!modelContext) return;
33205
+ this.stop();
33206
+ const generation = ++this.#generation;
33207
+ void startWebMcpBridge({ client, modelContext, enabled: true }).then((bridge) => {
33208
+ if (generation === this.#generation) this.#bridge = bridge;
33209
+ else bridge.stop();
33210
+ });
33211
+ }
33212
+ stop() {
33213
+ this.#generation += 1;
33214
+ this.#bridge?.stop();
33215
+ this.#bridge = void 0;
33216
+ }
33217
+ };
33218
+ function browserModelContext() {
33219
+ if (typeof document === "undefined") return void 0;
33220
+ const host = document;
33221
+ return typeof host.modelContext?.registerTool === "function" ? host.modelContext : void 0;
33222
+ }
33223
+
32937
33224
  // src/presentation-styles.ts
32938
33225
  var presentationStyles = `<style>
32939
33226
  :host([data-mode="floating"]) {
@@ -33177,9 +33464,18 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33177
33464
  #turnInFlight = false;
33178
33465
  #fetch;
33179
33466
  #hostAppearance;
33467
+ #webmcp = new AssistantElementWebMcpController();
33180
33468
  #appearanceWarningKeys = /* @__PURE__ */ new Set();
33181
33469
  #appearanceStyleSnapshot = /* @__PURE__ */ new Map();
33182
33470
  #scroll = new AssistantElementScrollController();
33471
+ #suggestions = new AssistantElementSuggestionsController({
33472
+ root: () => this.shadowRoot,
33473
+ appearance: () => this.#appearance,
33474
+ client: () => this.#client,
33475
+ hasMessages: () => this.hasAttribute("has-messages"),
33476
+ isOpen: () => this.hasAttribute("open"),
33477
+ send: (prompt) => this.#sendGuarded(prompt)
33478
+ });
33183
33479
  sessionEndpoint = "";
33184
33480
  /** Public mount: the non-secret embed id `noodle deploy` printed. Mutually exclusive with the above. */
33185
33481
  embedId = "";
@@ -33291,6 +33587,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33291
33587
  this.#launcher.disconnect();
33292
33588
  this.#setBusy(false);
33293
33589
  this.#environment.disconnect();
33590
+ this.#webmcp.stop();
33294
33591
  }
33295
33592
  attributeChangedCallback(name21) {
33296
33593
  if (name21 === "theme") this.#applyTheme();
@@ -33324,6 +33621,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33324
33621
  }
33325
33622
  resetSession() {
33326
33623
  this.#conversationGeneration += 1;
33624
+ this.#webmcp.stop();
33327
33625
  this.#client?.abort();
33328
33626
  this.#unsubscribeClient?.();
33329
33627
  this.#client = void 0;
@@ -33333,12 +33631,14 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33333
33631
  this.#events.resetConversation();
33334
33632
  this.#pendingUserEcho = void 0;
33335
33633
  this.removeAttribute("has-messages");
33634
+ this.#suggestions.reset();
33336
33635
  this.#setBusy(false);
33337
33636
  this.#setSessionState("idle");
33338
33637
  }
33339
33638
  /** Re-establish the session without replaying a message or interaction decision. */
33340
33639
  reconnect() {
33341
33640
  this.#conversationGeneration += 1;
33641
+ this.#webmcp.stop();
33342
33642
  this.#client?.abort();
33343
33643
  this.#unsubscribeClient?.();
33344
33644
  this.#client = void 0;
@@ -33396,6 +33696,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33396
33696
  this.#turnInFlight = true;
33397
33697
  this.#pendingUserEcho = message;
33398
33698
  this.#appendMessage("user", message);
33699
+ this.#suggestions.clear();
33399
33700
  this.#setBusy(true);
33400
33701
  if (this.#sessionState !== "ready") this.#setSessionState("loading");
33401
33702
  try {
@@ -33415,12 +33716,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33415
33716
  if (generation === this.#conversationGeneration) this.#setBusy(false);
33416
33717
  }
33417
33718
  }
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
- */
33719
+ /** DOM listeners swallow after the failing path dispatches its typed assistant-error event. */
33424
33720
  #sendGuarded(text3) {
33425
33721
  void this.sendMessage(text3).catch(() => {
33426
33722
  });
@@ -33472,13 +33768,21 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33472
33768
  }
33473
33769
  #primeSession() {
33474
33770
  if (!this.sessionEndpoint && !this.embedId) return;
33475
- if (this.#sessionBootstrap || this.#client?.hasSession()) return;
33771
+ if (this.#sessionBootstrap) return;
33772
+ if (this.#client?.hasSession()) {
33773
+ this.#suggestions.maybeLoadInitial();
33774
+ return;
33775
+ }
33476
33776
  if (this.#client?.isBusy?.() === true) return;
33477
33777
  if (this.embedId && !this.#pageContext) void this.#automaticPageContext.refresh();
33478
33778
  const client = this.#ensureClient();
33479
33779
  const generation = this.#conversationGeneration;
33480
33780
  this.#setSessionState("loading");
33481
- const bootstrap = client.connect().catch((error51) => {
33781
+ const bootstrap = client.connect().then(() => {
33782
+ if (generation === this.#conversationGeneration && this.#client === client) {
33783
+ this.#suggestions.maybeLoadInitial();
33784
+ }
33785
+ }).catch((error51) => {
33482
33786
  if (generation !== this.#conversationGeneration || this.#client !== client) return;
33483
33787
  this.#dispatchClientError(error51, "session_failed");
33484
33788
  this.#presentationGate.reveal();
@@ -33490,15 +33794,18 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33490
33794
  #handleClientEvent(event) {
33491
33795
  this.dispatchEvent(new CustomEvent("assistant-event", { detail: event }));
33492
33796
  this.#events.handle(event);
33797
+ this.#suggestions.handle(event);
33493
33798
  if (event.event === "session_started") {
33494
33799
  this.#publicConfiguration.disconnect();
33495
33800
  this.removeAttribute("data-public-configuration-loading");
33496
33801
  this.#presentationGate.reveal();
33497
33802
  this.#setSessionState("ready");
33498
33803
  this.#messages?.querySelector(".conversation-error")?.remove();
33804
+ this.#webmcp.start(this.#client);
33499
33805
  }
33500
33806
  if (event.event === "session_expired" || event.event === "session_reset") {
33501
33807
  this.#setSessionState("idle");
33808
+ this.#webmcp.stop();
33502
33809
  }
33503
33810
  if (event.event === "tool_proposed" || event.event === "interaction_proposed" || event.event === "input_requested") {
33504
33811
  this.#removeThinking();
@@ -33602,23 +33909,17 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33602
33909
  if (error51 instanceof AssistantClientError && error51.detail.code === "session_failed") {
33603
33910
  this.#setSessionState("error");
33604
33911
  }
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);
33912
+ this.#dispatchError(
33913
+ renderConversationError({
33914
+ messages: this.#messages,
33915
+ appearance: this.#appearance,
33916
+ error: error51,
33917
+ fallbackCode,
33918
+ startNewConversation: () => this.#startNewConversation(),
33919
+ reconnect: () => this.reconnect(),
33920
+ revealLatest: () => this.#revealLatest()
33921
+ })
33922
+ );
33622
33923
  }
33623
33924
  #startNewConversation() {
33624
33925
  this.resetSession();
@@ -33651,7 +33952,9 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33651
33952
  }
33652
33953
  }
33653
33954
  #applyConfiguration(configuration) {
33955
+ this.#webmcp.configure(configuration.assistant?.webmcp?.enabled === true);
33654
33956
  this.#appearance = resolveAppearance(configuration);
33957
+ this.#suggestions.render();
33655
33958
  this.#syncAppearance();
33656
33959
  this.#applyStartOpenPolicy();
33657
33960
  }
@@ -33696,15 +33999,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33696
33999
  "aria-label",
33697
34000
  appearance.labels.close
33698
34001
  );
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
- }
34002
+ this.#suggestions.render();
33708
34003
  queryRequired(this.shadowRoot, "header").hidden = !appearance.behavior.showHeader;
33709
34004
  const legal = queryRequired(this.shadowRoot, ".legal");
33710
34005
  legal.replaceChildren();
@@ -33742,14 +34037,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
33742
34037
  textarea.placeholder = appearance.labels.composerPlaceholder;
33743
34038
  textarea.setAttribute("aria-label", appearance.labels.composerPlaceholder);
33744
34039
  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
- }
34040
+ this.#suggestions.render();
33753
34041
  if (!appearance.behavior.showLauncher)
33754
34042
  queryRequired(this.shadowRoot, ".launcher").hidden = true;
33755
34043
  if (!appearance.behavior.showHeader)