@noodleseed/assistant 1.20.0 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { A as AssistantPageContext, a as AssistantContext, C as CreateAssistantClientOptions, b as AssistantChatState, c as AssistantClient } from '../client-7o7n2nqw.cjs';
1
+ import { A as AssistantPageContext, a as AssistantContext, C as CreateAssistantClientOptions, b as AssistantChatState, c as AssistantClient } from '../client-BZ7xPwfW.cjs';
2
2
  import '../appearance-Cc-eqSbp.cjs';
3
3
  import 'ai';
4
4
 
@@ -1,4 +1,4 @@
1
- import { A as AssistantPageContext, a as AssistantContext, C as CreateAssistantClientOptions, b as AssistantChatState, c as AssistantClient } from '../client-CysBQBdd.js';
1
+ import { A as AssistantPageContext, a as AssistantContext, C as CreateAssistantClientOptions, b as AssistantChatState, c as AssistantClient } from '../client-BkJlpN5N.js';
2
2
  import '../appearance-Cc-eqSbp.js';
3
3
  import 'ai';
4
4
 
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  createAssistantClient
4
- } from "../chunk-52XT7BLR.js";
4
+ } from "../chunk-ZPZHPXIQ.js";
5
5
  import "../chunk-3YYTUJPJ.js";
6
6
  import "../chunk-5FUTL2UF.js";
7
7
 
package/dist/react.cjs CHANGED
@@ -27033,6 +27033,12 @@ var AssistantChatStateStore = class {
27033
27033
  case "interaction_started":
27034
27034
  this.#beginInteraction(event.data.id);
27035
27035
  return;
27036
+ case "resume_started":
27037
+ this.#status = "submitted";
27038
+ this.#error = void 0;
27039
+ this.#startOperation();
27040
+ this.#notify();
27041
+ return;
27036
27042
  case "content":
27037
27043
  this.#writeText(event.data.delta);
27038
27044
  return;
@@ -27288,6 +27294,78 @@ function isRecord(value) {
27288
27294
  return typeof value === "object" && value !== null && !Array.isArray(value);
27289
27295
  }
27290
27296
 
27297
+ // src/client-errors.ts
27298
+ async function refusalCode(response) {
27299
+ try {
27300
+ const body = await response.clone().json();
27301
+ const code = body.code;
27302
+ return typeof code === "string" ? code : void 0;
27303
+ } catch {
27304
+ return void 0;
27305
+ }
27306
+ }
27307
+ var UNRETRYABLE_SERVICE_CODES = /* @__PURE__ */ new Set([
27308
+ "daily_turn_budget_exhausted",
27309
+ "daily_session_budget_exhausted"
27310
+ ]);
27311
+ var AssistantClientError = class extends Error {
27312
+ detail;
27313
+ constructor(detail, message, options) {
27314
+ super(message, options);
27315
+ this.name = "AssistantClientError";
27316
+ this.detail = detail;
27317
+ }
27318
+ };
27319
+ function parseSession(value) {
27320
+ if (!isRecord2(value) || !isRecord2(value.endpoints)) {
27321
+ throw clientError("session_failed", "Assistant session response is invalid", true);
27322
+ }
27323
+ const interactions = value.endpoints.interactions;
27324
+ const apps = value.endpoints.apps;
27325
+ const sandbox = value.endpoints.sandbox;
27326
+ 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") {
27327
+ throw clientError("session_failed", "Assistant session response is invalid", true);
27328
+ }
27329
+ return {
27330
+ token: value.token,
27331
+ expiresAt: value.expiresAt,
27332
+ endpoints: {
27333
+ turns: value.endpoints.turns,
27334
+ toolConfirmations: value.endpoints.toolConfirmations,
27335
+ ...typeof interactions === "string" ? { interactions } : {},
27336
+ ...typeof apps === "string" ? { apps } : {},
27337
+ ...typeof sandbox === "string" ? { sandbox } : {}
27338
+ },
27339
+ ...isRecord2(value.configuration) ? { configuration: value.configuration } : {},
27340
+ ...isRecord2(value.resume) && typeof value.resume.tool === "string" ? { resume: { tool: value.resume.tool } } : {}
27341
+ };
27342
+ }
27343
+ function isRecord2(value) {
27344
+ return typeof value === "object" && value !== null && !Array.isArray(value);
27345
+ }
27346
+ async function readStableErrorCode(response) {
27347
+ let value;
27348
+ try {
27349
+ value = await response.json();
27350
+ } catch {
27351
+ return void 0;
27352
+ }
27353
+ if (!isRecord2(value) || typeof value.code !== "string") return void 0;
27354
+ return /^[A-Za-z0-9_.-]{1,64}$/.test(value.code) ? value.code : void 0;
27355
+ }
27356
+ function clientError(code, message, retryable, status, options, serviceCode) {
27357
+ return new AssistantClientError(
27358
+ {
27359
+ code,
27360
+ ...status === void 0 ? {} : { status },
27361
+ retryable,
27362
+ ...serviceCode === void 0 ? {} : { serviceCode }
27363
+ },
27364
+ message,
27365
+ options
27366
+ );
27367
+ }
27368
+
27291
27369
  // src/events.ts
27292
27370
  function toAssistantClientEvent(event) {
27293
27371
  const value = event.data;
@@ -27303,12 +27381,12 @@ function toAssistantClientEvent(event) {
27303
27381
  }
27304
27382
  break;
27305
27383
  case "tool_proposed":
27306
- if (hasString(value, "id") && hasString(value, "tool") && hasOptionalString(value.title) && hasOptionalString(value.description) && hasOptionalJson(value.arguments) && (value.reviewSchema === void 0 || isRecord2(value.reviewSchema)) && hasOptionalString(value.expiresAt) && (value.requiresConfirmation === void 0 || value.requiresConfirmation === true) && hasOptionalTurnId(value)) {
27384
+ if (hasString(value, "id") && hasString(value, "tool") && hasOptionalString(value.title) && hasOptionalString(value.description) && hasOptionalJson(value.arguments) && (value.reviewSchema === void 0 || isRecord3(value.reviewSchema)) && hasOptionalString(value.expiresAt) && (value.requiresConfirmation === void 0 || value.requiresConfirmation === true) && hasOptionalTurnId(value)) {
27307
27385
  return event;
27308
27386
  }
27309
27387
  break;
27310
27388
  case "input_requested":
27311
- if (hasString(value, "id") && hasString(value, "message") && isRecord2(value.requestedSchema) && hasString(value, "expiresAt") && hasOptionalTurnId(value)) {
27389
+ if (hasString(value, "id") && hasString(value, "message") && isRecord3(value.requestedSchema) && hasString(value, "expiresAt") && hasOptionalTurnId(value)) {
27312
27390
  return event;
27313
27391
  }
27314
27392
  break;
@@ -27352,7 +27430,7 @@ function toAssistantClientEvent(event) {
27352
27430
  return { event: "unrecognized", data: { name: event.event, payload: value } };
27353
27431
  }
27354
27432
  function isViewAvailableDetail(value) {
27355
- 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 || isRecord2(value.resourceMeta)) && (value.allowedOpenDomains === void 0 || Array.isArray(value.allowedOpenDomains) && value.allowedOpenDomains.every(isHttpsUrl)) && hasOptionalTurnId(value);
27433
+ 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(isHttpsUrl)) && hasOptionalTurnId(value);
27356
27434
  }
27357
27435
  function isHttpsUrl(value) {
27358
27436
  if (typeof value !== "string") return false;
@@ -27385,11 +27463,11 @@ function isJsonValue(value, depth) {
27385
27463
  if (Array.isArray(value)) {
27386
27464
  return value.length <= 256 && value.every((entry) => isJsonValue(entry, depth + 1));
27387
27465
  }
27388
- if (!isRecord2(value)) return false;
27466
+ if (!isRecord3(value)) return false;
27389
27467
  const entries2 = Object.entries(value);
27390
27468
  return entries2.length <= 256 && entries2.every(([, entry]) => isJsonValue(entry, depth + 1));
27391
27469
  }
27392
- function isRecord2(value) {
27470
+ function isRecord3(value) {
27393
27471
  return typeof value === "object" && value !== null && !Array.isArray(value);
27394
27472
  }
27395
27473
 
@@ -27519,15 +27597,15 @@ function parseFrame(frame) {
27519
27597
  if (event === "done" && !isValidDonePayload(parsed)) {
27520
27598
  throw invalidStream("assistant event stream contained an invalid done event");
27521
27599
  }
27522
- return { event, data: isRecord3(parsed) ? parsed : { value: parsed } };
27600
+ return { event, data: isRecord4(parsed) ? parsed : { value: parsed } };
27523
27601
  }
27524
27602
  function normalizeLineEndings(input) {
27525
27603
  return input.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
27526
27604
  }
27527
27605
  function isValidDonePayload(value) {
27528
- return isRecord3(value) && (value.turnId === void 0 || typeof value.turnId === "string" && value.turnId.length > 0);
27606
+ return isRecord4(value) && (value.turnId === void 0 || typeof value.turnId === "string" && value.turnId.length > 0);
27529
27607
  }
27530
- function isRecord3(value) {
27608
+ function isRecord4(value) {
27531
27609
  return typeof value === "object" && value !== null && !Array.isArray(value);
27532
27610
  }
27533
27611
  function invalidStream(message, cause) {
@@ -27538,27 +27616,6 @@ function isAbortError2(error51) {
27538
27616
  }
27539
27617
 
27540
27618
  // src/client.ts
27541
- async function refusalCode(response) {
27542
- try {
27543
- const body = await response.clone().json();
27544
- const code = body.code;
27545
- return typeof code === "string" ? code : void 0;
27546
- } catch {
27547
- return void 0;
27548
- }
27549
- }
27550
- var UNRETRYABLE_SERVICE_CODES = /* @__PURE__ */ new Set([
27551
- "daily_turn_budget_exhausted",
27552
- "daily_session_budget_exhausted"
27553
- ]);
27554
- var AssistantClientError = class extends Error {
27555
- detail;
27556
- constructor(detail, message, options) {
27557
- super(message, options);
27558
- this.name = "AssistantClientError";
27559
- this.detail = detail;
27560
- }
27561
- };
27562
27619
  var DefaultAssistantClient = class {
27563
27620
  #source;
27564
27621
  #fetch;
@@ -27593,12 +27650,50 @@ var DefaultAssistantClient = class {
27593
27650
  hasSession() {
27594
27651
  return this.#session !== void 0;
27595
27652
  }
27653
+ /** Whether a request holds the single-flight slot, so callers can refuse before mutating UI. */
27654
+ isBusy() {
27655
+ return this.#active !== void 0;
27656
+ }
27596
27657
  async connect() {
27597
27658
  if (this.#session) return;
27598
27659
  await this.#singleFlight(async (signal) => {
27599
- if (!this.#session) await this.#createSession(signal);
27660
+ if (this.#session) return;
27661
+ const session = await this.#createSession(signal);
27662
+ if (session.resume) {
27663
+ try {
27664
+ await this.#runChatOperation(signal, () => this.#resumeTurn(session, signal));
27665
+ } catch {
27666
+ }
27667
+ }
27600
27668
  });
27601
27669
  }
27670
+ /** POST the one-shot resume trigger; a 409 means nothing was pending, which renders as silence. */
27671
+ async #resumeTurn(session, signal) {
27672
+ const response = await this.#request(
27673
+ session.endpoints.turns,
27674
+ {
27675
+ method: "POST",
27676
+ headers: authorizedHeaders(session.token, "text/event-stream"),
27677
+ body: JSON.stringify({ resume: true }),
27678
+ signal
27679
+ },
27680
+ "turn_failed"
27681
+ );
27682
+ if (response.status === 409) return;
27683
+ if (!response.ok) {
27684
+ throw clientError(
27685
+ "turn_failed",
27686
+ `Assistant turn failed (${response.status})`,
27687
+ false,
27688
+ response.status,
27689
+ void 0,
27690
+ await refusalCode(response)
27691
+ );
27692
+ }
27693
+ this.#emit({ event: "resume_started", data: { tool: session.resume?.tool ?? "" } });
27694
+ await this.#consume(response);
27695
+ this.#emit({ event: "message_completed", data: {} });
27696
+ }
27602
27697
  async sendMessage(text3) {
27603
27698
  const message = text3.trim();
27604
27699
  if (!message) return;
@@ -28003,10 +28098,10 @@ function authorizedHeaders(token, accept) {
28003
28098
  };
28004
28099
  }
28005
28100
  function parseAppInteraction(value) {
28006
- if (!isRecord4(value) || !isRecord4(value.interaction)) return void 0;
28101
+ if (!isRecord2(value) || !isRecord2(value.interaction)) return void 0;
28007
28102
  const event = value.interaction.event;
28008
28103
  const data = value.interaction.data;
28009
- if (event !== "tool_proposed" && event !== "input_requested" || !isRecord4(data)) {
28104
+ if (event !== "tool_proposed" && event !== "input_requested" || !isRecord2(data)) {
28010
28105
  return void 0;
28011
28106
  }
28012
28107
  const parsed = toAssistantClientEvent({ event, data });
@@ -28026,54 +28121,6 @@ function appInteractionStopped(action) {
28026
28121
  isError: true
28027
28122
  };
28028
28123
  }
28029
- function parseSession(value) {
28030
- if (!isRecord4(value) || !isRecord4(value.endpoints)) {
28031
- throw clientError("session_failed", "Assistant session response is invalid", true);
28032
- }
28033
- const interactions = value.endpoints.interactions;
28034
- const apps = value.endpoints.apps;
28035
- const sandbox = value.endpoints.sandbox;
28036
- 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") {
28037
- throw clientError("session_failed", "Assistant session response is invalid", true);
28038
- }
28039
- return {
28040
- token: value.token,
28041
- expiresAt: value.expiresAt,
28042
- endpoints: {
28043
- turns: value.endpoints.turns,
28044
- toolConfirmations: value.endpoints.toolConfirmations,
28045
- ...typeof interactions === "string" ? { interactions } : {},
28046
- ...typeof apps === "string" ? { apps } : {},
28047
- ...typeof sandbox === "string" ? { sandbox } : {}
28048
- },
28049
- ...isRecord4(value.configuration) ? { configuration: value.configuration } : {}
28050
- };
28051
- }
28052
- function isRecord4(value) {
28053
- return typeof value === "object" && value !== null && !Array.isArray(value);
28054
- }
28055
- async function readStableErrorCode(response) {
28056
- let value;
28057
- try {
28058
- value = await response.json();
28059
- } catch {
28060
- return void 0;
28061
- }
28062
- if (!isRecord4(value) || typeof value.code !== "string") return void 0;
28063
- return /^[A-Za-z0-9_.-]{1,64}$/.test(value.code) ? value.code : void 0;
28064
- }
28065
- function clientError(code, message, retryable, status, options, serviceCode) {
28066
- return new AssistantClientError(
28067
- {
28068
- code,
28069
- ...status === void 0 ? {} : { status },
28070
- retryable,
28071
- ...serviceCode === void 0 ? {} : { serviceCode }
28072
- },
28073
- message,
28074
- options
28075
- );
28076
- }
28077
28124
 
28078
28125
  // src/input-request-card.ts
28079
28126
  function createInputRequestCard(options) {
@@ -31258,6 +31305,11 @@ var AssistantElementEventController = class {
31258
31305
  this.#completedTool = void 0;
31259
31306
  return;
31260
31307
  }
31308
+ if (name21 === "resume_started") {
31309
+ this.#startStream("message");
31310
+ this.#host.element.dispatchEvent(new CustomEvent("assistant-message-started"));
31311
+ return;
31312
+ }
31261
31313
  if (name21 === "interaction_completed") {
31262
31314
  if (this.#streamKind !== "interaction") return;
31263
31315
  this.#finishInteraction(String(data.id ?? ""), String(data.action ?? "accept"));
@@ -31476,6 +31528,36 @@ function queryRequired(root, selector) {
31476
31528
  if (!element) throw new Error(`assistant renderer is missing ${selector}`);
31477
31529
  return element;
31478
31530
  }
31531
+ function assistantElementMarkup(appearance, open, styles) {
31532
+ return `${styles}
31533
+ <button class="launcher" type="button" aria-label="${escapeAttribute(appearance.labels.open)}" aria-controls="assistant-panel" aria-expanded="${String(open)}"><slot name="launcher-icon"></slot><span class="visually-hidden" data-session-status aria-live="polite" hidden></span></button>
31534
+ <section class="panel" id="assistant-panel" role="dialog" aria-label="${escapeAttribute(appearance.brand.name)}">
31535
+ <header><slot name="header-leading"></slot><img class="brand-logo" alt=""><strong></strong><slot name="header-actions"></slot><button class="close" type="button" aria-label="${escapeAttribute(appearance.labels.close)}"></button></header>
31536
+ <div class="welcome"><slot name="empty-state"></slot></div>
31537
+ <div class="suggested-prompts"></div>
31538
+ <div class="messages-region">
31539
+ <div class="messages" aria-live="polite"></div>
31540
+ <button class="new-messages" type="button" aria-label="${escapeAttribute(appearance.labels.newMessages)}" hidden></button>
31541
+ </div>
31542
+ <form><slot name="composer-leading"></slot><textarea rows="1"></textarea><button class="send" type="submit"></button><slot name="composer-trailing"></slot></form>
31543
+ <nav class="legal" aria-label="Legal"></nav>
31544
+ <slot name="conversation-footer"></slot>
31545
+ </section>`;
31546
+ }
31547
+ function appendLegalLinks(legal, appearance) {
31548
+ for (const [label, href] of [
31549
+ ["Privacy", appearance.privacyUrl],
31550
+ ["Terms", appearance.termsUrl]
31551
+ ]) {
31552
+ if (!href) continue;
31553
+ const link = document.createElement("a");
31554
+ link.href = href;
31555
+ link.target = "_blank";
31556
+ link.rel = "noopener noreferrer";
31557
+ link.textContent = label;
31558
+ legal.append(link);
31559
+ }
31560
+ }
31479
31561
 
31480
31562
  // src/element-page-context-controller.ts
31481
31563
  var AssistantElementPageContextController = class {
@@ -31517,6 +31599,33 @@ var AssistantElementPageContextController = class {
31517
31599
  }
31518
31600
  };
31519
31601
 
31602
+ // src/element-presentation-gate.ts
31603
+ var AssistantElementPresentationGate = class {
31604
+ #host;
31605
+ #primeSession;
31606
+ #focusComposer;
31607
+ constructor(host, primeSession, focusComposer) {
31608
+ this.#host = host;
31609
+ this.#primeSession = primeSession;
31610
+ this.#focusComposer = focusComposer;
31611
+ }
31612
+ syncOpenState() {
31613
+ const open = this.#host.hasAttribute("open");
31614
+ const presentationReady = this.#host.hasAttribute("data-presentation-ready");
31615
+ const launcher = this.#host.shadowRoot?.querySelector(".launcher");
31616
+ launcher?.setAttribute("aria-expanded", String(open && presentationReady));
31617
+ launcher?.setAttribute("aria-busy", String(open && !presentationReady));
31618
+ if (launcher) launcher.disabled = open && !presentationReady;
31619
+ if (open && this.#host.isConnected) this.#primeSession();
31620
+ }
31621
+ reveal() {
31622
+ if (this.#host.hasAttribute("data-presentation-ready")) return;
31623
+ this.#host.setAttribute("data-presentation-ready", "");
31624
+ this.syncOpenState();
31625
+ if (this.#host.hasAttribute("open")) queueMicrotask(this.#focusComposer);
31626
+ }
31627
+ };
31628
+
31520
31629
  // src/element-scroll-controller.ts
31521
31630
  var AssistantElementScrollController = class {
31522
31631
  #region;
@@ -31632,9 +31741,10 @@ var ASSISTANT_ELEMENT_STYLES = `<style>
31632
31741
  .launcher:hover { transform: translateY(-2px) scale(1.03); box-shadow: 0 8px 26px rgba(0,0,0,.16); }
31633
31742
  .launcher img { width: 28px; height: 28px; border-radius: 50%; object-fit: contain; }
31634
31743
  .panel { display: none; flex-direction: column; width: min(var(--ns-assistant-panel-width, var(--ns-assistant-default-panel-width)), calc(100vw - 40px)); min-height: var(--ns-assistant-min-height, var(--ns-assistant-default-min-height)); max-height: min(var(--ns-assistant-max-height, var(--ns-assistant-default-max-height)), calc(100vh - 40px)); overflow: hidden; color: var(--ns-assistant-panel-text, var(--ns-assistant-text, var(--ns-assistant-default-text))); background: var(--ns-assistant-panel, var(--ns-assistant-default-panel)); border: 1px solid var(--ns-assistant-panel-border, var(--ns-assistant-divider, var(--ns-assistant-default-divider))); border-radius: var(--ns-assistant-panel-radius, var(--ns-assistant-default-panel-radius)); box-shadow: var(--ns-assistant-shadow, var(--ns-assistant-default-shadow)); transform-origin: bottom right; isolation: isolate; animation: halo-panel-in .28s cubic-bezier(.23,1,.32,1) both; }
31635
- :host([open]) .panel { display: flex; }
31744
+ :host([open][data-presentation-ready]) .panel { display: flex; }
31636
31745
  .panel { position: relative; }
31637
- :host([open]) .launcher { display: none; }
31746
+ :host([open][data-presentation-ready]) .launcher { display: none; }
31747
+ :host([open]:not([data-presentation-ready])) .launcher { cursor: progress; opacity: .72; transform: none; }
31638
31748
  header { display: flex; flex: 0 0 auto; gap: 10px; align-items: center; min-height: 64px; padding: 12px 18px; color: var(--ns-assistant-header-text, inherit); background: var(--ns-assistant-header, transparent); border-bottom: 1px solid color-mix(in srgb, var(--ns-assistant-header-border, var(--ns-assistant-divider, var(--ns-assistant-default-divider))) 78%, transparent); }
31639
31749
  header strong { flex: 1; font-size: 16px; font-weight: 600; letter-spacing: 0; }
31640
31750
  .brand-logo { display: block; max-width: 120px; max-height: 28px; object-fit: contain; }
@@ -32260,9 +32370,16 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32260
32370
  #automaticPageContext = new AssistantElementPageContextController(
32261
32371
  () => !this.#pageContext
32262
32372
  );
32373
+ #presentationGate = new AssistantElementPresentationGate(
32374
+ this,
32375
+ () => this.#primeSession(),
32376
+ () => this.focusComposer()
32377
+ );
32263
32378
  #pendingUserEcho;
32264
32379
  #sessionState = "idle";
32265
32380
  #startOpenApplied = false;
32381
+ #readyDispatched = false;
32382
+ #turnInFlight = false;
32266
32383
  #hostAppearance;
32267
32384
  #appearanceWarningKeys = /* @__PURE__ */ new Set();
32268
32385
  #appearanceStyleSnapshot = /* @__PURE__ */ new Map();
@@ -32328,20 +32445,24 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32328
32445
  if (!this.sessionEndpoint) this.sessionEndpoint = this.getAttribute("session-endpoint") ?? "";
32329
32446
  if (!this.embedId) this.embedId = this.getAttribute("embed-id") ?? "";
32330
32447
  if (!this.serviceUrl) this.serviceUrl = this.getAttribute("service-url") ?? "";
32448
+ if (!this.sessionEndpoint && !this.embedId) this.#presentationGate.reveal();
32331
32449
  this.#media = globalThis.matchMedia?.("(prefers-color-scheme: dark)");
32332
32450
  this.#media?.addEventListener("change", this.#handleSystemTheme);
32333
32451
  document.addEventListener("keydown", this.#handleDocumentKeydown);
32334
32452
  document.addEventListener("pointerdown", this.#handleDocumentPointerDown);
32335
32453
  if (this.#appearance.behavior.startOpen) this.#applyStartOpenPolicy();
32336
32454
  this.#render();
32337
- this.dispatchEvent(new CustomEvent("assistant-ready"));
32455
+ if (!this.#readyDispatched) {
32456
+ this.#readyDispatched = true;
32457
+ this.dispatchEvent(new CustomEvent("assistant-ready"));
32458
+ }
32338
32459
  if (this.hasAttribute("start-open")) {
32339
32460
  this.setAttribute("open", "");
32340
32461
  this.#startOpenApplied = true;
32341
32462
  } else if (!this.embedId) {
32342
32463
  this.#primeSession();
32343
32464
  }
32344
- this.#syncOpenState();
32465
+ this.#presentationGate.syncOpenState();
32345
32466
  }
32346
32467
  disconnectedCallback() {
32347
32468
  this.#conversationGeneration += 1;
@@ -32359,7 +32480,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32359
32480
  }
32360
32481
  attributeChangedCallback(name21) {
32361
32482
  if (name21 === "theme") this.#applyTheme();
32362
- if (name21 === "open") this.#syncOpenState();
32483
+ if (name21 === "open") this.#presentationGate.syncOpenState();
32363
32484
  }
32364
32485
  open() {
32365
32486
  this.setAttribute("open", "");
@@ -32448,7 +32569,14 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32448
32569
  async sendMessage(text3) {
32449
32570
  const message = text3.trim();
32450
32571
  if (!message) return;
32572
+ if (this.#turnInFlight) {
32573
+ throw new AssistantClientError(
32574
+ { code: "request_in_progress", retryable: true },
32575
+ "an assistant request is already in progress"
32576
+ );
32577
+ }
32451
32578
  const generation = this.#conversationGeneration;
32579
+ this.#turnInFlight = true;
32452
32580
  this.#pendingUserEcho = message;
32453
32581
  this.#appendMessage("user", message);
32454
32582
  this.#setBusy(true);
@@ -32465,6 +32593,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32465
32593
  this.#dispatchClientError(error51, "turn_failed");
32466
32594
  throw error51;
32467
32595
  } finally {
32596
+ this.#turnInFlight = false;
32468
32597
  if (this.#pendingUserEcho === message) this.#pendingUserEcho = void 0;
32469
32598
  if (generation === this.#conversationGeneration) this.#setBusy(false);
32470
32599
  }
@@ -32533,6 +32662,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32533
32662
  #primeSession() {
32534
32663
  if (!this.sessionEndpoint && !this.embedId) return;
32535
32664
  if (this.#sessionBootstrap || this.#client?.hasSession()) return;
32665
+ if (this.#client?.isBusy?.() === true) return;
32536
32666
  if (this.embedId && !this.#pageContext) void this.#automaticPageContext.refresh();
32537
32667
  const client = this.#ensureClient();
32538
32668
  const generation = this.#conversationGeneration;
@@ -32540,6 +32670,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32540
32670
  const bootstrap = client.connect().catch((error51) => {
32541
32671
  if (generation !== this.#conversationGeneration || this.#client !== client) return;
32542
32672
  this.#dispatchClientError(error51, "session_failed");
32673
+ this.#presentationGate.reveal();
32543
32674
  }).finally(() => {
32544
32675
  if (this.#sessionBootstrap === bootstrap) this.#sessionBootstrap = void 0;
32545
32676
  });
@@ -32549,6 +32680,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32549
32680
  this.dispatchEvent(new CustomEvent("assistant-event", { detail: event }));
32550
32681
  this.#events.handle(event);
32551
32682
  if (event.event === "session_started") {
32683
+ this.#presentationGate.reveal();
32552
32684
  this.#setSessionState("ready");
32553
32685
  this.#messages?.querySelector(".conversation-error")?.remove();
32554
32686
  }
@@ -32562,6 +32694,8 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32562
32694
  if (event.event === "content" || event.event === "message_completed" || event.event === "interaction_completed" || event.event === "error") {
32563
32695
  this.#removeThinking();
32564
32696
  }
32697
+ if (event.event === "resume_started") this.#setBusy(true);
32698
+ if (event.event === "message_completed" || event.event === "error") this.#setBusy(false);
32565
32699
  }
32566
32700
  #setBusy(busy) {
32567
32701
  this.toggleAttribute("busy", busy);
@@ -32716,11 +32850,6 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32716
32850
  this.#sessionState = state;
32717
32851
  syncAssistantSessionVisualState(this, this.shadowRoot, state, this.#appearance);
32718
32852
  }
32719
- #syncOpenState() {
32720
- const open = this.hasAttribute("open");
32721
- this.shadowRoot?.querySelector(".launcher")?.setAttribute("aria-expanded", String(open));
32722
- if (open && this.isConnected) this.#primeSession();
32723
- }
32724
32853
  #closeAndRestoreFocus() {
32725
32854
  this.close();
32726
32855
  this.shadowRoot?.querySelector(".launcher")?.focus();
@@ -32770,7 +32899,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32770
32899
  queryRequired(this.shadowRoot, "header").hidden = !appearance.behavior.showHeader;
32771
32900
  const legal = queryRequired(this.shadowRoot, ".legal");
32772
32901
  legal.replaceChildren();
32773
- this.#appendLegalLinks(legal);
32902
+ appendLegalLinks(legal, this.#appearance);
32774
32903
  this.#applyTheme();
32775
32904
  }
32776
32905
  #render() {
@@ -32786,20 +32915,11 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32786
32915
  this.dataset.position = appearance.layout.position;
32787
32916
  this.dataset.density = appearance.layout.density;
32788
32917
  this.toggleAttribute("mobile-fullscreen", appearance.layout.mobileFullscreen);
32789
- this.shadowRoot.innerHTML = `${ASSISTANT_ELEMENT_STYLES}${presentationStyles}
32790
- <button class="launcher" type="button" aria-label="${escapeAttribute(appearance.labels.open)}" aria-controls="assistant-panel" aria-expanded="${String(this.hasAttribute("open"))}"><slot name="launcher-icon"></slot><span class="visually-hidden" data-session-status aria-live="polite" hidden></span></button>
32791
- <section class="panel" id="assistant-panel" role="dialog" aria-label="${escapeAttribute(appearance.brand.name)}">
32792
- <header><slot name="header-leading"></slot><img class="brand-logo" alt=""><strong></strong><slot name="header-actions"></slot><button class="close" type="button" aria-label="${escapeAttribute(appearance.labels.close)}"></button></header>
32793
- <div class="welcome"><slot name="empty-state"></slot></div>
32794
- <div class="suggested-prompts"></div>
32795
- <div class="messages-region">
32796
- <div class="messages" aria-live="polite"></div>
32797
- <button class="new-messages" type="button" aria-label="${escapeAttribute(appearance.labels.newMessages)}" hidden></button>
32798
- </div>
32799
- <form><slot name="composer-leading"></slot><textarea rows="1"></textarea><button class="send" type="submit"></button><slot name="composer-trailing"></slot></form>
32800
- <nav class="legal" aria-label="Legal"></nav>
32801
- <slot name="conversation-footer"></slot>
32802
- </section>`;
32918
+ this.shadowRoot.innerHTML = assistantElementMarkup(
32919
+ appearance,
32920
+ this.hasAttribute("open"),
32921
+ `${ASSISTANT_ELEMENT_STYLES}${presentationStyles}`
32922
+ );
32803
32923
  queryRequired(this.shadowRoot, "strong").textContent = appearance.brand.name;
32804
32924
  const logo = queryRequired(this.shadowRoot, ".brand-logo");
32805
32925
  const logoUrl = appearance.brand.logo[this.#resolvedMode()];
@@ -32826,7 +32946,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32826
32946
  if (!appearance.behavior.showHeader)
32827
32947
  queryRequired(this.shadowRoot, "header").hidden = true;
32828
32948
  const legal = queryRequired(this.shadowRoot, ".legal");
32829
- this.#appendLegalLinks(legal);
32949
+ appendLegalLinks(legal, this.#appearance);
32830
32950
  this.#messages = this.shadowRoot.querySelector(".messages") ?? void 0;
32831
32951
  this.#messages?.append(...conversation);
32832
32952
  this.#thinking = this.#messages?.querySelector(".thinking") ?? void 0;
@@ -32865,20 +32985,6 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32865
32985
  });
32866
32986
  this.#applyTheme();
32867
32987
  }
32868
- #appendLegalLinks(legal) {
32869
- for (const [label, href] of [
32870
- ["Privacy", this.#appearance.privacyUrl],
32871
- ["Terms", this.#appearance.termsUrl]
32872
- ]) {
32873
- if (!href) continue;
32874
- const link = document.createElement("a");
32875
- link.href = href;
32876
- link.target = "_blank";
32877
- link.rel = "noopener noreferrer";
32878
- link.textContent = label;
32879
- legal.append(link);
32880
- }
32881
- }
32882
32988
  };
32883
32989
  function registerNoodleAssistant() {
32884
32990
  if (globalThis.customElements && !customElements.get(ASSISTANT_TAG_NAME)) {
@@ -32948,6 +33054,7 @@ var NoodleAssistantWithRef = (0, import_react2.forwardRef)(
32948
33054
  const ready = () => {
32949
33055
  if (readyNotified.current) return;
32950
33056
  readyNotified.current = true;
33057
+ console.debug("[noodle-assistant] ready");
32951
33058
  props.onReady?.();
32952
33059
  };
32953
33060
  const error51 = (event) => props.onError?.(event.detail);