@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.
package/dist/index.cjs CHANGED
@@ -27030,6 +27030,12 @@ var AssistantChatStateStore = class {
27030
27030
  case "interaction_started":
27031
27031
  this.#beginInteraction(event.data.id);
27032
27032
  return;
27033
+ case "resume_started":
27034
+ this.#status = "submitted";
27035
+ this.#error = void 0;
27036
+ this.#startOperation();
27037
+ this.#notify();
27038
+ return;
27033
27039
  case "content":
27034
27040
  this.#writeText(event.data.delta);
27035
27041
  return;
@@ -27285,6 +27291,78 @@ function isRecord(value) {
27285
27291
  return typeof value === "object" && value !== null && !Array.isArray(value);
27286
27292
  }
27287
27293
 
27294
+ // src/client-errors.ts
27295
+ async function refusalCode(response) {
27296
+ try {
27297
+ const body = await response.clone().json();
27298
+ const code = body.code;
27299
+ return typeof code === "string" ? code : void 0;
27300
+ } catch {
27301
+ return void 0;
27302
+ }
27303
+ }
27304
+ var UNRETRYABLE_SERVICE_CODES = /* @__PURE__ */ new Set([
27305
+ "daily_turn_budget_exhausted",
27306
+ "daily_session_budget_exhausted"
27307
+ ]);
27308
+ var AssistantClientError = class extends Error {
27309
+ detail;
27310
+ constructor(detail, message, options) {
27311
+ super(message, options);
27312
+ this.name = "AssistantClientError";
27313
+ this.detail = detail;
27314
+ }
27315
+ };
27316
+ function parseSession(value) {
27317
+ if (!isRecord2(value) || !isRecord2(value.endpoints)) {
27318
+ throw clientError("session_failed", "Assistant session response is invalid", true);
27319
+ }
27320
+ const interactions = value.endpoints.interactions;
27321
+ const apps = value.endpoints.apps;
27322
+ const sandbox = value.endpoints.sandbox;
27323
+ 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") {
27324
+ throw clientError("session_failed", "Assistant session response is invalid", true);
27325
+ }
27326
+ return {
27327
+ token: value.token,
27328
+ expiresAt: value.expiresAt,
27329
+ endpoints: {
27330
+ turns: value.endpoints.turns,
27331
+ toolConfirmations: value.endpoints.toolConfirmations,
27332
+ ...typeof interactions === "string" ? { interactions } : {},
27333
+ ...typeof apps === "string" ? { apps } : {},
27334
+ ...typeof sandbox === "string" ? { sandbox } : {}
27335
+ },
27336
+ ...isRecord2(value.configuration) ? { configuration: value.configuration } : {},
27337
+ ...isRecord2(value.resume) && typeof value.resume.tool === "string" ? { resume: { tool: value.resume.tool } } : {}
27338
+ };
27339
+ }
27340
+ function isRecord2(value) {
27341
+ return typeof value === "object" && value !== null && !Array.isArray(value);
27342
+ }
27343
+ async function readStableErrorCode(response) {
27344
+ let value;
27345
+ try {
27346
+ value = await response.json();
27347
+ } catch {
27348
+ return void 0;
27349
+ }
27350
+ if (!isRecord2(value) || typeof value.code !== "string") return void 0;
27351
+ return /^[A-Za-z0-9_.-]{1,64}$/.test(value.code) ? value.code : void 0;
27352
+ }
27353
+ function clientError(code, message, retryable, status, options, serviceCode) {
27354
+ return new AssistantClientError(
27355
+ {
27356
+ code,
27357
+ ...status === void 0 ? {} : { status },
27358
+ retryable,
27359
+ ...serviceCode === void 0 ? {} : { serviceCode }
27360
+ },
27361
+ message,
27362
+ options
27363
+ );
27364
+ }
27365
+
27288
27366
  // src/events.ts
27289
27367
  function toAssistantClientEvent(event) {
27290
27368
  const value = event.data;
@@ -27300,12 +27378,12 @@ function toAssistantClientEvent(event) {
27300
27378
  }
27301
27379
  break;
27302
27380
  case "tool_proposed":
27303
- 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)) {
27381
+ 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)) {
27304
27382
  return event;
27305
27383
  }
27306
27384
  break;
27307
27385
  case "input_requested":
27308
- if (hasString(value, "id") && hasString(value, "message") && isRecord2(value.requestedSchema) && hasString(value, "expiresAt") && hasOptionalTurnId(value)) {
27386
+ if (hasString(value, "id") && hasString(value, "message") && isRecord3(value.requestedSchema) && hasString(value, "expiresAt") && hasOptionalTurnId(value)) {
27309
27387
  return event;
27310
27388
  }
27311
27389
  break;
@@ -27349,7 +27427,7 @@ function toAssistantClientEvent(event) {
27349
27427
  return { event: "unrecognized", data: { name: event.event, payload: value } };
27350
27428
  }
27351
27429
  function isViewAvailableDetail(value) {
27352
- 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);
27430
+ 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);
27353
27431
  }
27354
27432
  function isHttpsUrl(value) {
27355
27433
  if (typeof value !== "string") return false;
@@ -27382,11 +27460,11 @@ function isJsonValue(value, depth) {
27382
27460
  if (Array.isArray(value)) {
27383
27461
  return value.length <= 256 && value.every((entry) => isJsonValue(entry, depth + 1));
27384
27462
  }
27385
- if (!isRecord2(value)) return false;
27463
+ if (!isRecord3(value)) return false;
27386
27464
  const entries2 = Object.entries(value);
27387
27465
  return entries2.length <= 256 && entries2.every(([, entry]) => isJsonValue(entry, depth + 1));
27388
27466
  }
27389
- function isRecord2(value) {
27467
+ function isRecord3(value) {
27390
27468
  return typeof value === "object" && value !== null && !Array.isArray(value);
27391
27469
  }
27392
27470
 
@@ -27516,15 +27594,15 @@ function parseFrame(frame) {
27516
27594
  if (event === "done" && !isValidDonePayload(parsed)) {
27517
27595
  throw invalidStream("assistant event stream contained an invalid done event");
27518
27596
  }
27519
- return { event, data: isRecord3(parsed) ? parsed : { value: parsed } };
27597
+ return { event, data: isRecord4(parsed) ? parsed : { value: parsed } };
27520
27598
  }
27521
27599
  function normalizeLineEndings(input) {
27522
27600
  return input.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
27523
27601
  }
27524
27602
  function isValidDonePayload(value) {
27525
- return isRecord3(value) && (value.turnId === void 0 || typeof value.turnId === "string" && value.turnId.length > 0);
27603
+ return isRecord4(value) && (value.turnId === void 0 || typeof value.turnId === "string" && value.turnId.length > 0);
27526
27604
  }
27527
- function isRecord3(value) {
27605
+ function isRecord4(value) {
27528
27606
  return typeof value === "object" && value !== null && !Array.isArray(value);
27529
27607
  }
27530
27608
  function invalidStream(message, cause) {
@@ -27535,27 +27613,6 @@ function isAbortError2(error51) {
27535
27613
  }
27536
27614
 
27537
27615
  // src/client.ts
27538
- async function refusalCode(response) {
27539
- try {
27540
- const body = await response.clone().json();
27541
- const code = body.code;
27542
- return typeof code === "string" ? code : void 0;
27543
- } catch {
27544
- return void 0;
27545
- }
27546
- }
27547
- var UNRETRYABLE_SERVICE_CODES = /* @__PURE__ */ new Set([
27548
- "daily_turn_budget_exhausted",
27549
- "daily_session_budget_exhausted"
27550
- ]);
27551
- var AssistantClientError = class extends Error {
27552
- detail;
27553
- constructor(detail, message, options) {
27554
- super(message, options);
27555
- this.name = "AssistantClientError";
27556
- this.detail = detail;
27557
- }
27558
- };
27559
27616
  var DefaultAssistantClient = class {
27560
27617
  #source;
27561
27618
  #fetch;
@@ -27590,12 +27647,50 @@ var DefaultAssistantClient = class {
27590
27647
  hasSession() {
27591
27648
  return this.#session !== void 0;
27592
27649
  }
27650
+ /** Whether a request holds the single-flight slot, so callers can refuse before mutating UI. */
27651
+ isBusy() {
27652
+ return this.#active !== void 0;
27653
+ }
27593
27654
  async connect() {
27594
27655
  if (this.#session) return;
27595
27656
  await this.#singleFlight(async (signal) => {
27596
- if (!this.#session) await this.#createSession(signal);
27657
+ if (this.#session) return;
27658
+ const session = await this.#createSession(signal);
27659
+ if (session.resume) {
27660
+ try {
27661
+ await this.#runChatOperation(signal, () => this.#resumeTurn(session, signal));
27662
+ } catch {
27663
+ }
27664
+ }
27597
27665
  });
27598
27666
  }
27667
+ /** POST the one-shot resume trigger; a 409 means nothing was pending, which renders as silence. */
27668
+ async #resumeTurn(session, signal) {
27669
+ const response = await this.#request(
27670
+ session.endpoints.turns,
27671
+ {
27672
+ method: "POST",
27673
+ headers: authorizedHeaders(session.token, "text/event-stream"),
27674
+ body: JSON.stringify({ resume: true }),
27675
+ signal
27676
+ },
27677
+ "turn_failed"
27678
+ );
27679
+ if (response.status === 409) return;
27680
+ if (!response.ok) {
27681
+ throw clientError(
27682
+ "turn_failed",
27683
+ `Assistant turn failed (${response.status})`,
27684
+ false,
27685
+ response.status,
27686
+ void 0,
27687
+ await refusalCode(response)
27688
+ );
27689
+ }
27690
+ this.#emit({ event: "resume_started", data: { tool: session.resume?.tool ?? "" } });
27691
+ await this.#consume(response);
27692
+ this.#emit({ event: "message_completed", data: {} });
27693
+ }
27599
27694
  async sendMessage(text3) {
27600
27695
  const message = text3.trim();
27601
27696
  if (!message) return;
@@ -28000,10 +28095,10 @@ function authorizedHeaders(token, accept) {
28000
28095
  };
28001
28096
  }
28002
28097
  function parseAppInteraction(value) {
28003
- if (!isRecord4(value) || !isRecord4(value.interaction)) return void 0;
28098
+ if (!isRecord2(value) || !isRecord2(value.interaction)) return void 0;
28004
28099
  const event = value.interaction.event;
28005
28100
  const data = value.interaction.data;
28006
- if (event !== "tool_proposed" && event !== "input_requested" || !isRecord4(data)) {
28101
+ if (event !== "tool_proposed" && event !== "input_requested" || !isRecord2(data)) {
28007
28102
  return void 0;
28008
28103
  }
28009
28104
  const parsed = toAssistantClientEvent({ event, data });
@@ -28023,54 +28118,6 @@ function appInteractionStopped(action) {
28023
28118
  isError: true
28024
28119
  };
28025
28120
  }
28026
- function parseSession(value) {
28027
- if (!isRecord4(value) || !isRecord4(value.endpoints)) {
28028
- throw clientError("session_failed", "Assistant session response is invalid", true);
28029
- }
28030
- const interactions = value.endpoints.interactions;
28031
- const apps = value.endpoints.apps;
28032
- const sandbox = value.endpoints.sandbox;
28033
- 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") {
28034
- throw clientError("session_failed", "Assistant session response is invalid", true);
28035
- }
28036
- return {
28037
- token: value.token,
28038
- expiresAt: value.expiresAt,
28039
- endpoints: {
28040
- turns: value.endpoints.turns,
28041
- toolConfirmations: value.endpoints.toolConfirmations,
28042
- ...typeof interactions === "string" ? { interactions } : {},
28043
- ...typeof apps === "string" ? { apps } : {},
28044
- ...typeof sandbox === "string" ? { sandbox } : {}
28045
- },
28046
- ...isRecord4(value.configuration) ? { configuration: value.configuration } : {}
28047
- };
28048
- }
28049
- function isRecord4(value) {
28050
- return typeof value === "object" && value !== null && !Array.isArray(value);
28051
- }
28052
- async function readStableErrorCode(response) {
28053
- let value;
28054
- try {
28055
- value = await response.json();
28056
- } catch {
28057
- return void 0;
28058
- }
28059
- if (!isRecord4(value) || typeof value.code !== "string") return void 0;
28060
- return /^[A-Za-z0-9_.-]{1,64}$/.test(value.code) ? value.code : void 0;
28061
- }
28062
- function clientError(code, message, retryable, status, options, serviceCode) {
28063
- return new AssistantClientError(
28064
- {
28065
- code,
28066
- ...status === void 0 ? {} : { status },
28067
- retryable,
28068
- ...serviceCode === void 0 ? {} : { serviceCode }
28069
- },
28070
- message,
28071
- options
28072
- );
28073
- }
28074
28121
 
28075
28122
  // src/input-request-card.ts
28076
28123
  function createInputRequestCard(options) {
@@ -31255,6 +31302,11 @@ var AssistantElementEventController = class {
31255
31302
  this.#completedTool = void 0;
31256
31303
  return;
31257
31304
  }
31305
+ if (name21 === "resume_started") {
31306
+ this.#startStream("message");
31307
+ this.#host.element.dispatchEvent(new CustomEvent("assistant-message-started"));
31308
+ return;
31309
+ }
31258
31310
  if (name21 === "interaction_completed") {
31259
31311
  if (this.#streamKind !== "interaction") return;
31260
31312
  this.#finishInteraction(String(data.id ?? ""), String(data.action ?? "accept"));
@@ -31473,6 +31525,36 @@ function queryRequired(root, selector) {
31473
31525
  if (!element) throw new Error(`assistant renderer is missing ${selector}`);
31474
31526
  return element;
31475
31527
  }
31528
+ function assistantElementMarkup(appearance, open, styles) {
31529
+ return `${styles}
31530
+ <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>
31531
+ <section class="panel" id="assistant-panel" role="dialog" aria-label="${escapeAttribute(appearance.brand.name)}">
31532
+ <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>
31533
+ <div class="welcome"><slot name="empty-state"></slot></div>
31534
+ <div class="suggested-prompts"></div>
31535
+ <div class="messages-region">
31536
+ <div class="messages" aria-live="polite"></div>
31537
+ <button class="new-messages" type="button" aria-label="${escapeAttribute(appearance.labels.newMessages)}" hidden></button>
31538
+ </div>
31539
+ <form><slot name="composer-leading"></slot><textarea rows="1"></textarea><button class="send" type="submit"></button><slot name="composer-trailing"></slot></form>
31540
+ <nav class="legal" aria-label="Legal"></nav>
31541
+ <slot name="conversation-footer"></slot>
31542
+ </section>`;
31543
+ }
31544
+ function appendLegalLinks(legal, appearance) {
31545
+ for (const [label, href] of [
31546
+ ["Privacy", appearance.privacyUrl],
31547
+ ["Terms", appearance.termsUrl]
31548
+ ]) {
31549
+ if (!href) continue;
31550
+ const link = document.createElement("a");
31551
+ link.href = href;
31552
+ link.target = "_blank";
31553
+ link.rel = "noopener noreferrer";
31554
+ link.textContent = label;
31555
+ legal.append(link);
31556
+ }
31557
+ }
31476
31558
 
31477
31559
  // src/element-page-context-controller.ts
31478
31560
  var AssistantElementPageContextController = class {
@@ -31514,6 +31596,33 @@ var AssistantElementPageContextController = class {
31514
31596
  }
31515
31597
  };
31516
31598
 
31599
+ // src/element-presentation-gate.ts
31600
+ var AssistantElementPresentationGate = class {
31601
+ #host;
31602
+ #primeSession;
31603
+ #focusComposer;
31604
+ constructor(host, primeSession, focusComposer) {
31605
+ this.#host = host;
31606
+ this.#primeSession = primeSession;
31607
+ this.#focusComposer = focusComposer;
31608
+ }
31609
+ syncOpenState() {
31610
+ const open = this.#host.hasAttribute("open");
31611
+ const presentationReady = this.#host.hasAttribute("data-presentation-ready");
31612
+ const launcher = this.#host.shadowRoot?.querySelector(".launcher");
31613
+ launcher?.setAttribute("aria-expanded", String(open && presentationReady));
31614
+ launcher?.setAttribute("aria-busy", String(open && !presentationReady));
31615
+ if (launcher) launcher.disabled = open && !presentationReady;
31616
+ if (open && this.#host.isConnected) this.#primeSession();
31617
+ }
31618
+ reveal() {
31619
+ if (this.#host.hasAttribute("data-presentation-ready")) return;
31620
+ this.#host.setAttribute("data-presentation-ready", "");
31621
+ this.syncOpenState();
31622
+ if (this.#host.hasAttribute("open")) queueMicrotask(this.#focusComposer);
31623
+ }
31624
+ };
31625
+
31517
31626
  // src/element-scroll-controller.ts
31518
31627
  var AssistantElementScrollController = class {
31519
31628
  #region;
@@ -31629,9 +31738,10 @@ var ASSISTANT_ELEMENT_STYLES = `<style>
31629
31738
  .launcher:hover { transform: translateY(-2px) scale(1.03); box-shadow: 0 8px 26px rgba(0,0,0,.16); }
31630
31739
  .launcher img { width: 28px; height: 28px; border-radius: 50%; object-fit: contain; }
31631
31740
  .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; }
31632
- :host([open]) .panel { display: flex; }
31741
+ :host([open][data-presentation-ready]) .panel { display: flex; }
31633
31742
  .panel { position: relative; }
31634
- :host([open]) .launcher { display: none; }
31743
+ :host([open][data-presentation-ready]) .launcher { display: none; }
31744
+ :host([open]:not([data-presentation-ready])) .launcher { cursor: progress; opacity: .72; transform: none; }
31635
31745
  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); }
31636
31746
  header strong { flex: 1; font-size: 16px; font-weight: 600; letter-spacing: 0; }
31637
31747
  .brand-logo { display: block; max-width: 120px; max-height: 28px; object-fit: contain; }
@@ -32257,9 +32367,16 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32257
32367
  #automaticPageContext = new AssistantElementPageContextController(
32258
32368
  () => !this.#pageContext
32259
32369
  );
32370
+ #presentationGate = new AssistantElementPresentationGate(
32371
+ this,
32372
+ () => this.#primeSession(),
32373
+ () => this.focusComposer()
32374
+ );
32260
32375
  #pendingUserEcho;
32261
32376
  #sessionState = "idle";
32262
32377
  #startOpenApplied = false;
32378
+ #readyDispatched = false;
32379
+ #turnInFlight = false;
32263
32380
  #hostAppearance;
32264
32381
  #appearanceWarningKeys = /* @__PURE__ */ new Set();
32265
32382
  #appearanceStyleSnapshot = /* @__PURE__ */ new Map();
@@ -32325,20 +32442,24 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32325
32442
  if (!this.sessionEndpoint) this.sessionEndpoint = this.getAttribute("session-endpoint") ?? "";
32326
32443
  if (!this.embedId) this.embedId = this.getAttribute("embed-id") ?? "";
32327
32444
  if (!this.serviceUrl) this.serviceUrl = this.getAttribute("service-url") ?? "";
32445
+ if (!this.sessionEndpoint && !this.embedId) this.#presentationGate.reveal();
32328
32446
  this.#media = globalThis.matchMedia?.("(prefers-color-scheme: dark)");
32329
32447
  this.#media?.addEventListener("change", this.#handleSystemTheme);
32330
32448
  document.addEventListener("keydown", this.#handleDocumentKeydown);
32331
32449
  document.addEventListener("pointerdown", this.#handleDocumentPointerDown);
32332
32450
  if (this.#appearance.behavior.startOpen) this.#applyStartOpenPolicy();
32333
32451
  this.#render();
32334
- this.dispatchEvent(new CustomEvent("assistant-ready"));
32452
+ if (!this.#readyDispatched) {
32453
+ this.#readyDispatched = true;
32454
+ this.dispatchEvent(new CustomEvent("assistant-ready"));
32455
+ }
32335
32456
  if (this.hasAttribute("start-open")) {
32336
32457
  this.setAttribute("open", "");
32337
32458
  this.#startOpenApplied = true;
32338
32459
  } else if (!this.embedId) {
32339
32460
  this.#primeSession();
32340
32461
  }
32341
- this.#syncOpenState();
32462
+ this.#presentationGate.syncOpenState();
32342
32463
  }
32343
32464
  disconnectedCallback() {
32344
32465
  this.#conversationGeneration += 1;
@@ -32356,7 +32477,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32356
32477
  }
32357
32478
  attributeChangedCallback(name21) {
32358
32479
  if (name21 === "theme") this.#applyTheme();
32359
- if (name21 === "open") this.#syncOpenState();
32480
+ if (name21 === "open") this.#presentationGate.syncOpenState();
32360
32481
  }
32361
32482
  open() {
32362
32483
  this.setAttribute("open", "");
@@ -32445,7 +32566,14 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32445
32566
  async sendMessage(text3) {
32446
32567
  const message = text3.trim();
32447
32568
  if (!message) return;
32569
+ if (this.#turnInFlight) {
32570
+ throw new AssistantClientError(
32571
+ { code: "request_in_progress", retryable: true },
32572
+ "an assistant request is already in progress"
32573
+ );
32574
+ }
32448
32575
  const generation = this.#conversationGeneration;
32576
+ this.#turnInFlight = true;
32449
32577
  this.#pendingUserEcho = message;
32450
32578
  this.#appendMessage("user", message);
32451
32579
  this.#setBusy(true);
@@ -32462,6 +32590,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32462
32590
  this.#dispatchClientError(error51, "turn_failed");
32463
32591
  throw error51;
32464
32592
  } finally {
32593
+ this.#turnInFlight = false;
32465
32594
  if (this.#pendingUserEcho === message) this.#pendingUserEcho = void 0;
32466
32595
  if (generation === this.#conversationGeneration) this.#setBusy(false);
32467
32596
  }
@@ -32530,6 +32659,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32530
32659
  #primeSession() {
32531
32660
  if (!this.sessionEndpoint && !this.embedId) return;
32532
32661
  if (this.#sessionBootstrap || this.#client?.hasSession()) return;
32662
+ if (this.#client?.isBusy?.() === true) return;
32533
32663
  if (this.embedId && !this.#pageContext) void this.#automaticPageContext.refresh();
32534
32664
  const client = this.#ensureClient();
32535
32665
  const generation = this.#conversationGeneration;
@@ -32537,6 +32667,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32537
32667
  const bootstrap = client.connect().catch((error51) => {
32538
32668
  if (generation !== this.#conversationGeneration || this.#client !== client) return;
32539
32669
  this.#dispatchClientError(error51, "session_failed");
32670
+ this.#presentationGate.reveal();
32540
32671
  }).finally(() => {
32541
32672
  if (this.#sessionBootstrap === bootstrap) this.#sessionBootstrap = void 0;
32542
32673
  });
@@ -32546,6 +32677,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32546
32677
  this.dispatchEvent(new CustomEvent("assistant-event", { detail: event }));
32547
32678
  this.#events.handle(event);
32548
32679
  if (event.event === "session_started") {
32680
+ this.#presentationGate.reveal();
32549
32681
  this.#setSessionState("ready");
32550
32682
  this.#messages?.querySelector(".conversation-error")?.remove();
32551
32683
  }
@@ -32559,6 +32691,8 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32559
32691
  if (event.event === "content" || event.event === "message_completed" || event.event === "interaction_completed" || event.event === "error") {
32560
32692
  this.#removeThinking();
32561
32693
  }
32694
+ if (event.event === "resume_started") this.#setBusy(true);
32695
+ if (event.event === "message_completed" || event.event === "error") this.#setBusy(false);
32562
32696
  }
32563
32697
  #setBusy(busy) {
32564
32698
  this.toggleAttribute("busy", busy);
@@ -32713,11 +32847,6 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32713
32847
  this.#sessionState = state;
32714
32848
  syncAssistantSessionVisualState(this, this.shadowRoot, state, this.#appearance);
32715
32849
  }
32716
- #syncOpenState() {
32717
- const open = this.hasAttribute("open");
32718
- this.shadowRoot?.querySelector(".launcher")?.setAttribute("aria-expanded", String(open));
32719
- if (open && this.isConnected) this.#primeSession();
32720
- }
32721
32850
  #closeAndRestoreFocus() {
32722
32851
  this.close();
32723
32852
  this.shadowRoot?.querySelector(".launcher")?.focus();
@@ -32767,7 +32896,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32767
32896
  queryRequired(this.shadowRoot, "header").hidden = !appearance.behavior.showHeader;
32768
32897
  const legal = queryRequired(this.shadowRoot, ".legal");
32769
32898
  legal.replaceChildren();
32770
- this.#appendLegalLinks(legal);
32899
+ appendLegalLinks(legal, this.#appearance);
32771
32900
  this.#applyTheme();
32772
32901
  }
32773
32902
  #render() {
@@ -32783,20 +32912,11 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32783
32912
  this.dataset.position = appearance.layout.position;
32784
32913
  this.dataset.density = appearance.layout.density;
32785
32914
  this.toggleAttribute("mobile-fullscreen", appearance.layout.mobileFullscreen);
32786
- this.shadowRoot.innerHTML = `${ASSISTANT_ELEMENT_STYLES}${presentationStyles}
32787
- <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>
32788
- <section class="panel" id="assistant-panel" role="dialog" aria-label="${escapeAttribute(appearance.brand.name)}">
32789
- <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>
32790
- <div class="welcome"><slot name="empty-state"></slot></div>
32791
- <div class="suggested-prompts"></div>
32792
- <div class="messages-region">
32793
- <div class="messages" aria-live="polite"></div>
32794
- <button class="new-messages" type="button" aria-label="${escapeAttribute(appearance.labels.newMessages)}" hidden></button>
32795
- </div>
32796
- <form><slot name="composer-leading"></slot><textarea rows="1"></textarea><button class="send" type="submit"></button><slot name="composer-trailing"></slot></form>
32797
- <nav class="legal" aria-label="Legal"></nav>
32798
- <slot name="conversation-footer"></slot>
32799
- </section>`;
32915
+ this.shadowRoot.innerHTML = assistantElementMarkup(
32916
+ appearance,
32917
+ this.hasAttribute("open"),
32918
+ `${ASSISTANT_ELEMENT_STYLES}${presentationStyles}`
32919
+ );
32800
32920
  queryRequired(this.shadowRoot, "strong").textContent = appearance.brand.name;
32801
32921
  const logo = queryRequired(this.shadowRoot, ".brand-logo");
32802
32922
  const logoUrl = appearance.brand.logo[this.#resolvedMode()];
@@ -32823,7 +32943,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32823
32943
  if (!appearance.behavior.showHeader)
32824
32944
  queryRequired(this.shadowRoot, "header").hidden = true;
32825
32945
  const legal = queryRequired(this.shadowRoot, ".legal");
32826
- this.#appendLegalLinks(legal);
32946
+ appendLegalLinks(legal, this.#appearance);
32827
32947
  this.#messages = this.shadowRoot.querySelector(".messages") ?? void 0;
32828
32948
  this.#messages?.append(...conversation);
32829
32949
  this.#thinking = this.#messages?.querySelector(".thinking") ?? void 0;
@@ -32862,20 +32982,6 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32862
32982
  });
32863
32983
  this.#applyTheme();
32864
32984
  }
32865
- #appendLegalLinks(legal) {
32866
- for (const [label, href] of [
32867
- ["Privacy", this.#appearance.privacyUrl],
32868
- ["Terms", this.#appearance.termsUrl]
32869
- ]) {
32870
- if (!href) continue;
32871
- const link = document.createElement("a");
32872
- link.href = href;
32873
- link.target = "_blank";
32874
- link.rel = "noopener noreferrer";
32875
- link.textContent = label;
32876
- legal.append(link);
32877
- }
32878
- }
32879
32985
  };
32880
32986
  function registerNoodleAssistant() {
32881
32987
  if (globalThis.customElements && !customElements.get(ASSISTANT_TAG_NAME)) {