@noodleseed/assistant 1.4.0 → 1.6.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/react.cjs CHANGED
@@ -18330,12 +18330,12 @@ var common = {
18330
18330
  baseFontSize: 15,
18331
18331
  messageFontSize: 15,
18332
18332
  lineHeight: 1.5,
18333
- panelRadius: 24,
18334
- cardRadius: 16,
18335
- inputRadius: 999,
18336
- buttonRadius: 999,
18333
+ panelRadius: 20,
18334
+ cardRadius: 14,
18335
+ inputRadius: 14,
18336
+ buttonRadius: 12,
18337
18337
  launcherRadius: 999,
18338
- spacing: 12,
18338
+ spacing: 8,
18339
18339
  backdropBlur: 18,
18340
18340
  motionScale: 1
18341
18341
  };
@@ -18393,7 +18393,7 @@ var DEFAULT_APPEARANCE = {
18393
18393
  layout: {
18394
18394
  mode: "floating",
18395
18395
  position: "bottom-right",
18396
- panelWidth: 560,
18396
+ panelWidth: 520,
18397
18397
  panelMinHeight: 540,
18398
18398
  panelMaxHeight: 820,
18399
18399
  edgeOffset: 20,
@@ -18420,7 +18420,16 @@ var DEFAULT_APPEARANCE = {
18420
18420
  close: "Close assistant",
18421
18421
  open: "Open assistant",
18422
18422
  confirm: "Confirm",
18423
+ decline: "Don't proceed",
18423
18424
  cancel: "Cancel",
18425
+ confirmationHeading: "Review and confirm",
18426
+ additionalDetails: "Additional details",
18427
+ redacted: "Hidden for security",
18428
+ completed: "Completed",
18429
+ stopped: "Stopped",
18430
+ copy: "Copy",
18431
+ newMessages: "New messages",
18432
+ reconnect: "Reconnect",
18424
18433
  retry: "Try again",
18425
18434
  unavailable: "The assistant is temporarily unavailable.",
18426
18435
  sessionExpired: "Your assistant session expired. Reopen it to continue.",
@@ -18431,7 +18440,7 @@ var DEFAULT_APPEARANCE = {
18431
18440
  },
18432
18441
  presentation: {
18433
18442
  panel: {
18434
- surface: "glass",
18443
+ surface: "solid",
18435
18444
  elevation: "soft",
18436
18445
  border: "subtle"
18437
18446
  },
@@ -18602,7 +18611,7 @@ function toAssistantClientEvent(event) {
18602
18611
  }
18603
18612
  break;
18604
18613
  case "tool_proposed":
18605
- if (hasString(value, "id") && hasString(value, "tool") && hasOptionalJson(value.arguments) && hasOptionalString(value.expiresAt) && (value.requiresConfirmation === void 0 || value.requiresConfirmation === true) && hasOptionalTurnId(value)) {
18614
+ if (hasString(value, "id") && hasString(value, "tool") && hasOptionalString(value.title) && hasOptionalString(value.description) && hasOptionalJson(value.arguments) && (value.reviewSchema === void 0 || isRecord(value.reviewSchema)) && hasOptionalString(value.expiresAt) && (value.requiresConfirmation === void 0 || value.requiresConfirmation === true) && hasOptionalTurnId(value)) {
18606
18615
  return event;
18607
18616
  }
18608
18617
  break;
@@ -19581,22 +19590,54 @@ function record2(value) {
19581
19590
  function createInteractionCard(options) {
19582
19591
  const card = document.createElement("section");
19583
19592
  card.className = "tool-proposal";
19584
- const label = document.createElement("strong");
19585
- label.textContent = options.tool;
19586
- card.append(label);
19593
+ card.setAttribute("aria-label", options.labels.heading);
19594
+ const eyebrow = document.createElement("span");
19595
+ eyebrow.className = "proposal-eyebrow";
19596
+ eyebrow.textContent = options.labels.heading;
19597
+ const title = document.createElement("h3");
19598
+ title.textContent = options.title ?? humanizeArgumentName(options.tool);
19599
+ card.append(eyebrow, title);
19600
+ if (options.description) {
19601
+ const description = document.createElement("p");
19602
+ description.className = "proposal-description";
19603
+ description.textContent = options.description;
19604
+ card.append(description);
19605
+ }
19587
19606
  if (options.arguments && Object.keys(options.arguments).length > 0) {
19588
- card.append(createArgumentList(options.arguments));
19607
+ const { action, ...businessArguments } = options.arguments;
19608
+ if (Object.keys(businessArguments).length > 0) {
19609
+ card.append(createArgumentList(businessArguments, options.reviewSchema, options.labels));
19610
+ }
19611
+ if (action !== void 0) {
19612
+ const technical = document.createElement("details");
19613
+ technical.className = "proposal-details";
19614
+ const summary = document.createElement("summary");
19615
+ summary.textContent = options.labels.details;
19616
+ const properties = record3(options.reviewSchema?.properties);
19617
+ technical.append(summary);
19618
+ const actionObject = record3(action);
19619
+ if (actionObject) {
19620
+ technical.append(
19621
+ createArgumentList(actionObject, record3(properties?.action), options.labels)
19622
+ );
19623
+ } else {
19624
+ const value = document.createElement("div");
19625
+ appendValue(value, action, record3(properties?.action), options.labels);
19626
+ technical.append(value);
19627
+ }
19628
+ card.append(technical);
19629
+ }
19589
19630
  }
19590
19631
  const actions = document.createElement("div");
19591
19632
  actions.className = "proposal-actions";
19592
19633
  const buttons = [];
19593
19634
  for (const [labelText, action] of [
19594
19635
  [options.labels.accept, "accept"],
19595
- [options.labels.decline, "decline"],
19596
- [options.labels.cancel, "cancel"]
19636
+ [options.labels.decline, "decline"]
19597
19637
  ]) {
19598
19638
  const button2 = document.createElement("button");
19599
19639
  button2.type = "button";
19640
+ button2.className = `proposal-${action}`;
19600
19641
  button2.textContent = labelText;
19601
19642
  buttons.push(button2);
19602
19643
  button2.addEventListener("click", () => {
@@ -19610,27 +19651,98 @@ function createInteractionCard(options) {
19610
19651
  card.append(actions);
19611
19652
  return card;
19612
19653
  }
19613
- function createArgumentList(arguments_) {
19614
- const details = document.createElement("dl");
19615
- details.className = "proposal-arguments";
19654
+ function createResultCard(result, label, redactedLabel) {
19655
+ const card = document.createElement("section");
19656
+ card.className = "tool-result";
19657
+ const heading = document.createElement("strong");
19658
+ heading.textContent = label;
19659
+ card.append(heading);
19660
+ const body = document.createElement("div");
19661
+ const labels = {
19662
+ heading: label,
19663
+ accept: "",
19664
+ decline: "",
19665
+ details: "",
19666
+ redacted: redactedLabel
19667
+ };
19668
+ appendValue(body, result, void 0, labels);
19669
+ card.append(body);
19670
+ return card;
19671
+ }
19672
+ function createArgumentList(arguments_, schema, labels) {
19673
+ const list = document.createElement("dl");
19674
+ list.className = "proposal-arguments";
19675
+ const properties = record3(schema?.properties);
19616
19676
  for (const [name, value] of Object.entries(arguments_)) {
19677
+ const propertySchema = record3(properties?.[name]);
19617
19678
  const term = document.createElement("dt");
19618
- term.textContent = humanizeArgumentName(name);
19679
+ term.textContent = string4(propertySchema?.title) ?? humanizeArgumentName(name);
19680
+ const help = string4(propertySchema?.description);
19681
+ if (help) {
19682
+ const description2 = document.createElement("small");
19683
+ description2.textContent = help;
19684
+ term.append(description2);
19685
+ }
19619
19686
  const description = document.createElement("dd");
19620
- description.textContent = formatArgumentValue(value);
19621
- details.append(term, description);
19687
+ appendValue(description, value, propertySchema, labels);
19688
+ list.append(term, description);
19689
+ }
19690
+ return list;
19691
+ }
19692
+ function appendValue(parent, value, schema, labels) {
19693
+ if (value === "[REDACTED]") {
19694
+ const hidden = document.createElement("span");
19695
+ hidden.className = "proposal-redacted";
19696
+ hidden.textContent = labels.redacted;
19697
+ parent.append(hidden);
19698
+ return;
19699
+ }
19700
+ if (Array.isArray(value)) {
19701
+ const list = document.createElement("ul");
19702
+ const itemSchema = record3(schema?.items);
19703
+ for (const item of value) {
19704
+ const entry = document.createElement("li");
19705
+ appendValue(entry, item, itemSchema, labels);
19706
+ list.append(entry);
19707
+ }
19708
+ parent.append(list);
19709
+ return;
19710
+ }
19711
+ const objectValue = record3(value);
19712
+ if (objectValue) {
19713
+ parent.append(createArgumentList(objectValue, schema, labels));
19714
+ return;
19715
+ }
19716
+ parent.textContent = formatScalar(value, schema);
19717
+ }
19718
+ function formatScalar(value, schema) {
19719
+ if (value === null || value === void 0 || value === "") return "Not provided";
19720
+ if (typeof value === "boolean") return value ? "Yes" : "No";
19721
+ if (typeof value === "number") return new Intl.NumberFormat().format(value);
19722
+ if (typeof value !== "string") return String(value);
19723
+ if (schema?.format === "date") {
19724
+ const date5 = /* @__PURE__ */ new Date(`${value}T00:00:00`);
19725
+ if (!Number.isNaN(date5.valueOf()))
19726
+ return new Intl.DateTimeFormat(void 0, { dateStyle: "medium" }).format(date5);
19727
+ }
19728
+ if (schema?.format === "date-time") {
19729
+ const date5 = new Date(value);
19730
+ if (!Number.isNaN(date5.valueOf()))
19731
+ return new Intl.DateTimeFormat(void 0, { dateStyle: "medium", timeStyle: "short" }).format(
19732
+ date5
19733
+ );
19622
19734
  }
19623
- return details;
19735
+ return value;
19624
19736
  }
19625
19737
  function humanizeArgumentName(value) {
19626
19738
  const words = value.replaceAll(/([a-z0-9])([A-Z])/g, "$1 $2").replaceAll(/[_-]+/g, " ");
19627
19739
  return words ? `${words[0]?.toUpperCase() ?? ""}${words.slice(1)}` : value;
19628
19740
  }
19629
- function formatArgumentValue(value) {
19630
- if (value === null) return "None";
19631
- if (typeof value === "string") return value;
19632
- if (typeof value === "number" || typeof value === "boolean") return String(value);
19633
- return JSON.stringify(value) ?? "";
19741
+ function record3(value) {
19742
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
19743
+ }
19744
+ function string4(value) {
19745
+ return typeof value === "string" && value.length > 0 ? value : void 0;
19634
19746
  }
19635
19747
 
19636
19748
  // ../../node_modules/.pnpm/dompurify@3.4.11/node_modules/dompurify/dist/purify.es.mjs
@@ -22366,8 +22478,11 @@ var AssistantElementEventController = class {
22366
22478
  if (!this.#streamKind) return;
22367
22479
  this.#appendToolProposal(
22368
22480
  String(data.id ?? ""),
22369
- String(data.tool ?? ("title" in data ? data.title : void 0) ?? "Action"),
22370
- isRecord4(data.arguments) ? data.arguments : void 0
22481
+ String(data.tool ?? "action"),
22482
+ isRecord4(data.arguments) ? data.arguments : void 0,
22483
+ typeof data.title === "string" ? data.title : void 0,
22484
+ "description" in data && typeof data.description === "string" ? data.description : void 0,
22485
+ "reviewSchema" in data && isRecord4(data.reviewSchema) ? data.reviewSchema : void 0
22371
22486
  );
22372
22487
  return;
22373
22488
  }
@@ -22405,7 +22520,13 @@ var AssistantElementEventController = class {
22405
22520
  }
22406
22521
  #finishInteraction(id, action) {
22407
22522
  if (this.#completedTool && !this.#assistantBody) {
22408
- this.#host.appendMessage("assistant", JSON.stringify(this.#completedTool.result ?? {}) ?? "");
22523
+ const messages = this.#host.messages();
22524
+ if (messages) {
22525
+ const labels = this.#host.labels();
22526
+ messages.append(
22527
+ createResultCard(this.#completedTool.result, labels.completed, labels.redacted)
22528
+ );
22529
+ }
22409
22530
  }
22410
22531
  this.#proposalCards.get(id)?.remove();
22411
22532
  this.#proposalCards.delete(id);
@@ -22420,18 +22541,23 @@ var AssistantElementEventController = class {
22420
22541
  this.#clearStream();
22421
22542
  this.#completedTool = void 0;
22422
22543
  }
22423
- #appendToolProposal(id, tool, arguments_) {
22544
+ #appendToolProposal(id, tool, arguments_, title, description, reviewSchema) {
22424
22545
  const messages = this.#host.messages();
22425
22546
  if (!messages || !id) return;
22426
22547
  this.#proposalCards.get(id)?.remove();
22427
22548
  const labels = this.#host.labels();
22428
22549
  const card = createInteractionCard({
22429
22550
  tool,
22551
+ ...title ? { title } : {},
22552
+ ...description ? { description } : {},
22430
22553
  ...arguments_ ? { arguments: arguments_ } : {},
22554
+ ...reviewSchema ? { reviewSchema } : {},
22431
22555
  labels: {
22556
+ heading: labels.confirmationHeading,
22432
22557
  accept: labels.confirm,
22433
- decline: "Decline",
22434
- cancel: labels.cancel
22558
+ decline: labels.decline,
22559
+ details: labels.additionalDetails,
22560
+ redacted: labels.redacted
22435
22561
  },
22436
22562
  respond: (action) => this.#host.respond(id, { action })
22437
22563
  });
@@ -22439,7 +22565,9 @@ var AssistantElementEventController = class {
22439
22565
  this.#proposalCards.set(id, card);
22440
22566
  this.#revealLatestMessage(messages);
22441
22567
  this.#host.element.dispatchEvent(
22442
- new CustomEvent("assistant-tool-proposed", { detail: { id, tool, arguments: arguments_ } })
22568
+ new CustomEvent("assistant-tool-proposed", {
22569
+ detail: { id, tool, title, description, arguments: arguments_, reviewSchema }
22570
+ })
22443
22571
  );
22444
22572
  }
22445
22573
  #appendInputRequest(id, message, requestedSchema) {
@@ -22490,7 +22618,14 @@ var AssistantElementEventController = class {
22490
22618
  }
22491
22619
  #revealLatestMessage(messages) {
22492
22620
  this.#host.element.setAttribute("has-messages", "");
22493
- messages.scrollTop = messages.scrollHeight;
22621
+ const distance = messages.scrollHeight - messages.scrollTop - messages.clientHeight;
22622
+ const control = this.#host.element.shadowRoot?.querySelector(".new-messages");
22623
+ if (distance <= 160) {
22624
+ messages.scrollTop = messages.scrollHeight;
22625
+ if (control) control.hidden = true;
22626
+ } else if (control) {
22627
+ control.hidden = false;
22628
+ }
22494
22629
  }
22495
22630
  };
22496
22631
  function isRecord4(value) {
@@ -22525,17 +22660,18 @@ var ASSISTANT_ELEMENT_STYLES = `<style>
22525
22660
  :host([data-mode="floating"][data-position="bottom-left"]) { right: auto; left: 20px; }
22526
22661
  button { font: inherit; }
22527
22662
  button, textarea, input, select { -webkit-tap-highlight-color: transparent; }
22528
- .launcher { display: grid; place-items: center; width: 48px; height: 48px; border: 1px solid var(--ns-assistant-divider, var(--ns-assistant-default-divider)); border-radius: 999px; color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: color-mix(in srgb, var(--ns-assistant-panel, var(--ns-assistant-default-panel)) 82%, transparent); box-shadow: 0 4px 20px rgba(0,0,0,.12); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); cursor: pointer; transition: transform .5s cubic-bezier(.25,1,.5,1), box-shadow .2s ease; }
22663
+ .launcher { display: grid; place-items: center; width: 48px; height: 48px; border: 1px solid var(--ns-assistant-launcher-border, var(--ns-assistant-divider, var(--ns-assistant-default-divider))); border-radius: 999px; color: var(--ns-assistant-launcher-text, var(--ns-assistant-text, var(--ns-assistant-default-text))); background: var(--ns-assistant-launcher, var(--ns-assistant-panel, var(--ns-assistant-default-panel))); box-shadow: 0 8px 28px rgba(0,0,0,.14); cursor: pointer; transition: transform .24s cubic-bezier(.25,1,.5,1), box-shadow .2s ease; }
22529
22664
  .launcher:hover { transform: translateY(-2px) scale(1.03); box-shadow: 0 8px 26px rgba(0,0,0,.16); }
22530
22665
  .launcher img { width: 28px; height: 28px; border-radius: 50%; object-fit: contain; }
22531
- .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-text, var(--ns-assistant-default-text)); background: color-mix(in srgb, var(--ns-assistant-panel, var(--ns-assistant-default-panel)) 88%, transparent); border: 1px solid color-mix(in srgb, var(--ns-assistant-divider, var(--ns-assistant-default-divider)) 74%, transparent); border-radius: var(--ns-assistant-panel-radius, var(--ns-assistant-default-panel-radius)); box-shadow: var(--ns-assistant-shadow, var(--ns-assistant-default-shadow)); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); transform-origin: bottom right; animation: halo-panel-in .32s cubic-bezier(.23,1,.32,1) both; }
22666
+ .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; animation: halo-panel-in .28s cubic-bezier(.23,1,.32,1) both; }
22532
22667
  :host([open]) .panel { display: flex; }
22668
+ .panel { position: relative; }
22533
22669
  :host([open]) .launcher { display: none; }
22534
- header { display: flex; flex: 0 0 auto; gap: 10px; align-items: center; min-height: 64px; padding: 14px 18px; border-bottom: 1px solid var(--ns-assistant-divider, var(--ns-assistant-default-divider)); }
22670
+ 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 var(--ns-assistant-header-border, var(--ns-assistant-divider, var(--ns-assistant-default-divider))); }
22535
22671
  header strong { flex: 1; font-size: 16px; font-weight: 600; letter-spacing: -.01em; }
22536
22672
  .brand-logo { display: block; max-width: 120px; max-height: 28px; object-fit: contain; }
22537
22673
  .close, .send { display: grid; place-items: center; border: 0; border-radius: 999px; cursor: pointer; }
22538
- .close { width: 32px; height: 32px; padding: 0; color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); background: transparent; font-size: 24px; font-weight: 300; line-height: 1; transition: background .15s ease, color .15s ease; }
22674
+ .close { width: 44px; height: 44px; padding: 0; color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); background: transparent; font-size: 24px; font-weight: 300; line-height: 1; transition: background .15s ease, color .15s ease; }
22539
22675
  .close:hover { color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: var(--ns-assistant-elevated, var(--ns-assistant-default-elevated)); }
22540
22676
  .welcome { flex: 0 0 auto; padding: 30px 22px 18px; }
22541
22677
  .welcome h2 { margin: 0; font-size: 1.42em; line-height: 1.25; letter-spacing: -.025em; }
@@ -22543,16 +22679,24 @@ var ASSISTANT_ELEMENT_STYLES = `<style>
22543
22679
  :host([has-messages]) .welcome, :host([has-messages]) .suggested-prompts { display: none; }
22544
22680
  .suggested-prompts { display: flex; flex: 0 0 auto; flex-wrap: wrap; gap: 8px; padding: 0 20px 16px; }
22545
22681
  .suggested-prompts:empty { display: none; }
22546
- .suggested-prompts button { padding: 7px 12px; color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); background: transparent; border: 1px solid var(--ns-assistant-divider, var(--ns-assistant-default-divider)); border-radius: 999px; cursor: pointer; transition: background .2s ease, color .2s ease, transform .2s ease; }
22682
+ .suggested-prompts button { min-height: 44px; padding: 9px 14px; color: var(--ns-assistant-suggestion-text, var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text))); background: var(--ns-assistant-suggestion, transparent); border: 1px solid var(--ns-assistant-suggestion-border, var(--ns-assistant-divider, var(--ns-assistant-default-divider))); border-radius: 12px; cursor: pointer; transition: background .2s ease, color .2s ease, transform .2s ease; }
22547
22683
  .suggested-prompts button:hover { color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: var(--ns-assistant-elevated, var(--ns-assistant-default-elevated)); transform: translateY(-1px); }
22548
- .messages { flex: 1 1 auto; min-height: 0; padding: 18px 20px; overflow-y: auto; overscroll-behavior: contain; scroll-behavior: smooth; }
22684
+ .messages { flex: 1 1 auto; min-height: 0; padding: 18px 20px; overflow-y: auto; background: var(--ns-assistant-canvas, transparent); overscroll-behavior: contain; scroll-behavior: smooth; }
22549
22685
  .messages::-webkit-scrollbar { width: 6px; }
22550
22686
  .messages::-webkit-scrollbar-thumb { background: var(--ns-assistant-divider, var(--ns-assistant-default-divider)); border-radius: 999px; }
22551
22687
  .message { width: fit-content; max-width: 88%; margin: 0 0 14px; padding: 0; border-radius: var(--ns-assistant-card-radius, var(--ns-assistant-default-card-radius)); background: transparent; white-space: pre-wrap; animation: halo-message-in .2s ease both; }
22552
22688
  .message > span, .message > .markdown { overflow-wrap: anywhere; }
22553
22689
  .message .avatar { float: left; width: 24px; height: 24px; margin-right: 9px; border-radius: 50%; object-fit: cover; }
22554
22690
  .message time { display: block; margin-top: 4px; color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); font-size: .75em; line-height: 1.2; }
22555
- .message.user { display: grid; justify-items: start; gap: 3px; margin-left: auto; padding: 10px 16px 8px; color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: var(--ns-assistant-elevated, var(--ns-assistant-default-elevated)); font-size: .94em; }
22691
+ .message-copy { min-width: 44px; min-height: 32px; margin-top: 6px; padding: 4px 8px; color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); background: transparent; border: 0; border-radius: 8px; cursor: pointer; font-size: .78em; opacity: 0; transition: opacity .15s ease, background .15s ease; }
22692
+ .message:hover .message-copy, .message:focus-within .message-copy { opacity: 1; }
22693
+ .conversation-status { margin: 4px auto 14px; color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); font-size: .82em; text-align: center; }
22694
+ .conversation-error { display: flex; gap: 12px; align-items: center; justify-content: space-between; margin: 8px 0 16px; padding: 12px 14px; color: var(--ns-assistant-danger, var(--ns-assistant-default-danger)); border: 1px solid color-mix(in srgb, var(--ns-assistant-danger, var(--ns-assistant-default-danger)) 28%, transparent); border-radius: 12px; background: color-mix(in srgb, var(--ns-assistant-danger, var(--ns-assistant-default-danger)) 7%, transparent); }
22695
+ .conversation-error p { margin: 0; }
22696
+ .conversation-error button { min-height: 44px; padding: 8px 12px; color: var(--ns-assistant-secondary-button-text, inherit); background: var(--ns-assistant-secondary-button, var(--ns-assistant-elevated, var(--ns-assistant-default-elevated))); border: 1px solid var(--ns-assistant-secondary-button-border, var(--ns-assistant-divider, var(--ns-assistant-default-divider))); border-radius: 10px; cursor: pointer; font-weight: 600; }
22697
+ .new-messages { position: absolute; left: 50%; bottom: 82px; z-index: 1; min-height: 36px; padding: 7px 12px; color: var(--ns-assistant-secondary-button-text, inherit); background: var(--ns-assistant-secondary-button, var(--ns-assistant-elevated, var(--ns-assistant-default-elevated))); border: 1px solid var(--ns-assistant-secondary-button-border, var(--ns-assistant-divider, var(--ns-assistant-default-divider))); border-radius: 999px; box-shadow: 0 4px 18px rgba(0,0,0,.12); transform: translateX(-50%); cursor: pointer; }
22698
+ .message.assistant { color: var(--ns-assistant-assistant-message-text, inherit); background: var(--ns-assistant-assistant-message, transparent); border: 1px solid var(--ns-assistant-assistant-message-border, transparent); }
22699
+ .message.user { display: grid; justify-items: start; gap: 3px; margin-left: auto; padding: 10px 16px 8px; color: var(--ns-assistant-user-message-text, var(--ns-assistant-text, var(--ns-assistant-default-text))); background: var(--ns-assistant-user-message, var(--ns-assistant-elevated, var(--ns-assistant-default-elevated))); border: 1px solid var(--ns-assistant-user-message-border, transparent); font-size: .94em; }
22556
22700
  .message.user time { margin-top: 0; }
22557
22701
  .markdown { white-space: normal; }
22558
22702
  .markdown p { margin: 0 0 8px; }
@@ -22561,8 +22705,8 @@ var ASSISTANT_ELEMENT_STYLES = `<style>
22561
22705
  .markdown em, .markdown i { font-style: italic; }
22562
22706
  .markdown a { color: var(--ns-assistant-link, var(--ns-assistant-default-link)); text-decoration: underline; text-underline-offset: 2px; }
22563
22707
  .markdown a:hover { opacity: .8; }
22564
- .markdown code { padding: 2px 6px; border-radius: 5px; background: var(--ns-assistant-code, var(--ns-assistant-default-code)); font-family: var(--ns-assistant-mono-font-family, var(--ns-assistant-default-mono-font-family)); font-size: .87em; }
22565
- .markdown pre { margin: 10px 0; padding: 12px 14px; overflow-x: auto; border: 1px solid var(--ns-assistant-divider, var(--ns-assistant-default-divider)); border-radius: 10px; background: var(--ns-assistant-code, var(--ns-assistant-default-code)); white-space: pre; }
22708
+ .markdown code { padding: 2px 6px; color: var(--ns-assistant-code-text, inherit); border: 1px solid var(--ns-assistant-code-border, transparent); border-radius: 5px; background: var(--ns-assistant-code, var(--ns-assistant-default-code)); font-family: var(--ns-assistant-mono-font-family, var(--ns-assistant-default-mono-font-family)); font-size: .87em; }
22709
+ .markdown pre { margin: 10px 0; padding: 12px 14px; overflow-x: auto; color: var(--ns-assistant-code-text, inherit); border: 1px solid var(--ns-assistant-code-border, var(--ns-assistant-divider, var(--ns-assistant-default-divider))); border-radius: 10px; background: var(--ns-assistant-code, var(--ns-assistant-default-code)); white-space: pre; }
22566
22710
  .markdown pre code { padding: 0; background: transparent; font-size: .86em; }
22567
22711
  .markdown ul, .markdown ol { margin: 8px 0; padding-left: 22px; }
22568
22712
  .markdown li { margin: 3px 0; }
@@ -22571,15 +22715,25 @@ var ASSISTANT_ELEMENT_STYLES = `<style>
22571
22715
  .markdown h1:first-child, .markdown h2:first-child, .markdown h3:first-child { margin-top: 0; }
22572
22716
  .markdown h1 { font-size: 1.3em; } .markdown h2 { font-size: 1.18em; } .markdown h3 { font-size: 1.08em; }
22573
22717
  .message.thinking > span { display: inline-block; color: transparent; background: linear-gradient(90deg, var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)) 25%, var(--ns-assistant-text, var(--ns-assistant-default-text)) 50%, var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)) 75%); background-size: 300% 100%; background-clip: text; -webkit-background-clip: text; font-style: italic; font-weight: 500; animation: halo-shimmer 2s linear infinite; }
22574
- .tool-proposal { display: grid; gap: 10px; align-items: start; margin: 8px 0 14px; padding: 12px 14px; border: 1px solid var(--ns-assistant-divider, var(--ns-assistant-default-divider)); border-radius: 14px; background: color-mix(in srgb, var(--ns-assistant-input, var(--ns-assistant-default-input)) 78%, transparent); animation: halo-message-in .2s ease both; }
22575
- .tool-proposal > strong { font-weight: 650; overflow-wrap: anywhere; }
22576
- .proposal-arguments { display: grid; grid-template-columns: minmax(90px, auto) minmax(0, 1fr); gap: 4px 10px; margin: 0; font-size: .9em; }
22718
+ .tool-proposal { display: grid; gap: 12px; align-items: start; margin: 8px 0 16px; padding: 18px; color: var(--ns-assistant-confirmation-text, inherit); border: 1px solid var(--ns-assistant-confirmation-border, var(--ns-assistant-divider, var(--ns-assistant-default-divider))); border-radius: var(--ns-assistant-card-radius, 14px); background: var(--ns-assistant-confirmation, var(--ns-assistant-input, var(--ns-assistant-default-input))); animation: halo-message-in .2s ease both; }
22719
+ .proposal-eyebrow { color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); font-size: .75em; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
22720
+ .tool-proposal h3 { margin: -4px 0 0; font-size: 1.08em; line-height: 1.3; letter-spacing: -.01em; }
22721
+ .proposal-description { margin: -4px 0 0; color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); }
22722
+ .proposal-arguments { display: grid; grid-template-columns: minmax(100px, .7fr) minmax(0, 1.3fr); gap: 8px 14px; margin: 0; padding: 12px 0; border-block: 1px solid var(--ns-assistant-divider, var(--ns-assistant-default-divider)); font-size: .9em; }
22723
+ .proposal-arguments .proposal-arguments { grid-column: 1 / -1; padding: 4px 0 0; border: 0; }
22577
22724
  .proposal-arguments dt { color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); }
22725
+ .proposal-arguments dt small { display: block; margin-top: 2px; font-size: .85em; line-height: 1.35; }
22578
22726
  .proposal-arguments dd { margin: 0; overflow-wrap: anywhere; }
22727
+ .proposal-details { color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); font-size: .86em; }
22728
+ .proposal-details summary { min-height: 36px; padding: 8px 0; cursor: pointer; font-weight: 600; }
22729
+ .proposal-details .proposal-arguments { margin-top: 4px; }
22579
22730
  .proposal-actions { display: flex; flex-wrap: wrap; gap: 8px; justify-content: end; }
22580
- .tool-proposal button { padding: 7px 11px; border: 0; border-radius: 999px; color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: var(--ns-assistant-elevated, var(--ns-assistant-default-elevated)); cursor: pointer; }
22581
- .tool-proposal button:first-of-type { color: var(--ns-assistant-accent-text, var(--ns-assistant-default-accent-text)); background: var(--ns-assistant-accent, var(--ns-assistant-default-accent)); }
22731
+ .tool-proposal button { min-height: 44px; padding: 9px 14px; border: 1px solid var(--ns-assistant-secondary-button-border, transparent); border-radius: var(--ns-assistant-button-radius, 12px); color: var(--ns-assistant-secondary-button-text, var(--ns-assistant-text, var(--ns-assistant-default-text))); background: var(--ns-assistant-secondary-button, var(--ns-assistant-elevated, var(--ns-assistant-default-elevated))); cursor: pointer; font-weight: 600; }
22732
+ .tool-proposal .proposal-accept { color: var(--ns-assistant-primary-button-text, var(--ns-assistant-accent-text, var(--ns-assistant-default-accent-text))); background: var(--ns-assistant-primary-button, var(--ns-assistant-accent, var(--ns-assistant-default-accent))); border-color: var(--ns-assistant-primary-button-border, transparent); }
22582
22733
  .tool-proposal button:disabled { opacity: .55; cursor: not-allowed; }
22734
+ .tool-proposal button { min-height: 45px; }
22735
+ .tool-result { display: grid; gap: 10px; margin: 8px 0 16px; padding: 16px; border: 1px solid var(--ns-assistant-divider, var(--ns-assistant-default-divider)); border-radius: var(--ns-assistant-card-radius, 14px); background: var(--ns-assistant-assistant-message, var(--ns-assistant-elevated, var(--ns-assistant-default-elevated))); }
22736
+ .tool-result > strong { color: var(--ns-assistant-success, var(--ns-assistant-default-success)); }
22583
22737
  .input-request form { display: grid; gap: 10px; align-items: stretch; min-height: 0; margin: 0; padding: 0; border: 0; border-radius: 0; background: transparent; box-shadow: none; }
22584
22738
  .input-request form:focus-within { box-shadow: none; }
22585
22739
  .input-request label { display: grid; gap: 4px; color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); font-size: .9em; }
@@ -22588,20 +22742,21 @@ var ASSISTANT_ELEMENT_STYLES = `<style>
22588
22742
  .input-request input[type="checkbox"] { width: auto; justify-self: start; accent-color: var(--ns-assistant-accent, var(--ns-assistant-default-accent)); }
22589
22743
  .input-request input:focus-visible, .input-request select:focus-visible { border-color: var(--ns-assistant-focus, var(--ns-assistant-default-focus)); box-shadow: 0 0 0 2px var(--ns-assistant-focus, var(--ns-assistant-default-focus)); }
22590
22744
  .input-request .proposal-actions { margin-top: 2px; }
22591
- .noodle-app-card { position: relative; display: grid; gap: 8px; width: 100%; margin: 4px 0 16px; overflow: hidden; border: 1px solid var(--ns-assistant-divider, var(--ns-assistant-default-divider)); border-radius: var(--ns-assistant-card-radius, var(--ns-assistant-default-card-radius)); background: var(--ns-assistant-elevated, var(--ns-assistant-default-elevated)); }
22745
+ .noodle-app-card { position: relative; display: grid; gap: 8px; width: 100%; margin: 4px 0 16px; overflow: hidden; color: var(--ns-assistant-app-text, inherit); border: 1px solid var(--ns-assistant-app-border, var(--ns-assistant-divider, var(--ns-assistant-default-divider))); border-radius: var(--ns-assistant-card-radius, var(--ns-assistant-default-card-radius)); background: var(--ns-assistant-app, var(--ns-assistant-elevated, var(--ns-assistant-default-elevated))); }
22592
22746
  .noodle-app-title { padding: 10px 12px 0; font-size: .86em; font-weight: 650; }
22593
22747
  .noodle-app-frame { display: block; width: 100%; min-height: 120px; border: 0; background: transparent; }
22594
22748
  .noodle-app-card[data-fullscreen] { position: fixed; inset: 0; z-index: 2; margin: 0; border-radius: 0; background: var(--ns-assistant-panel, var(--ns-assistant-default-panel)); }
22595
22749
  .noodle-app-card[data-fullscreen] .noodle-app-frame { height: 100% !important; }
22596
- form { display: flex; flex: 0 0 auto; gap: 8px; align-items: center; min-height: 48px; margin: 8px 16px 12px; padding: 8px 8px 8px 16px; border: 0; border-radius: 999px; background: var(--ns-assistant-input, var(--ns-assistant-default-input)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ns-assistant-divider, var(--ns-assistant-default-divider)) 60%, transparent); transition: box-shadow .15s ease; }
22750
+ form { display: flex; flex: 0 0 auto; gap: 8px; align-items: center; min-height: 56px; margin: 8px 16px 16px; padding: 6px 6px 6px 16px; color: var(--ns-assistant-composer-text, inherit); border: 1px solid var(--ns-assistant-composer-border, var(--ns-assistant-divider, var(--ns-assistant-default-divider))); border-radius: var(--ns-assistant-input-radius, 14px); background: var(--ns-assistant-composer, var(--ns-assistant-input, var(--ns-assistant-default-input))); box-shadow: 0 1px 2px rgba(0,0,0,.04); transition: border-color .15s ease, box-shadow .15s ease; }
22597
22751
  form:focus-within { box-shadow: inset 0 0 0 1px var(--ns-assistant-focus, var(--ns-assistant-default-focus)), 0 0 0 2px var(--ns-assistant-focus, var(--ns-assistant-default-focus)); }
22598
22752
  textarea { flex: 1; min-height: 24px; max-height: 120px; resize: none; padding: 0; color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: transparent; border: 0; border-radius: 0; outline: 0; box-shadow: none; appearance: none; -webkit-appearance: none; font: inherit; font-size: 16px; line-height: 24px; }
22599
22753
  textarea::placeholder { color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); }
22600
22754
  textarea:focus, textarea:focus-visible { outline: none; box-shadow: none; }
22601
22755
  button:focus-visible { outline: 3px solid var(--ns-assistant-focus, var(--ns-assistant-default-focus)); outline-offset: 2px; }
22602
- .send { position: relative; display: block; width: 32px; height: 32px; flex: 0 0 32px; padding: 0; overflow: hidden; color: transparent; background: var(--ns-assistant-elevated, var(--ns-assistant-default-elevated)); font-size: 0; line-height: 0; transition: transform .15s ease, background .15s ease; }
22603
- .send::before { content: ''; position: absolute; top: 50%; left: 50%; width: 16px; height: 16px; background: var(--ns-assistant-text, var(--ns-assistant-default-text)); transform: translate(-50%, -50%); -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 19V5M5 12l7-7 7 7' fill='none' stroke='black' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat; mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 19V5M5 12l7-7 7 7' fill='none' stroke='black' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat; }
22756
+ .send { position: relative; display: block; width: 44px; height: 44px; flex: 0 0 44px; padding: 0; overflow: hidden; color: transparent; background: var(--ns-assistant-primary-button, var(--ns-assistant-accent, var(--ns-assistant-default-accent))); font-size: 0; line-height: 0; transition: transform .15s ease, background .15s ease; }
22757
+ .send::before { content: ''; position: absolute; top: 50%; left: 50%; width: 17px; height: 17px; background: var(--ns-assistant-primary-button-text, var(--ns-assistant-accent-text, var(--ns-assistant-default-accent-text))); transform: translate(-50%, -50%); -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 19V5M5 12l7-7 7 7' fill='none' stroke='black' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat; mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 19V5M5 12l7-7 7 7' fill='none' stroke='black' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat; }
22604
22758
  .send:hover { transform: scale(1.05); background: var(--ns-assistant-hover, var(--ns-assistant-default-hover)); }
22759
+ :host([busy]) .send::before { width: 13px; height: 13px; border-radius: 3px; -webkit-mask: none; mask: none; }
22605
22760
  .send:disabled, textarea:disabled { opacity: .55; cursor: not-allowed; }
22606
22761
  .legal { display: flex; gap: 12px; justify-content: center; padding: 0 12px 9px; font-size: .75em; }
22607
22762
  .legal:empty { display: none; }
@@ -22614,9 +22769,107 @@ var ASSISTANT_ELEMENT_STYLES = `<style>
22614
22769
  @media (max-width: 640px) { :host([mobile-fullscreen][open]) { inset: 0; } :host([mobile-fullscreen][open]) .panel { width: 100vw; min-height: 100vh; max-height: 100vh; border: 0; border-radius: 0; } }
22615
22770
  @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; } .messages { scroll-behavior: auto; } }
22616
22771
  @media (forced-colors: active) { .message.thinking > span { color: CanvasText; background: none; } }
22772
+ @media (hover: none) { .message-copy { opacity: 1; } }
22617
22773
  @media (max-width: 560px) { :host([data-mode="floating"]) { inset: auto 12px 12px; } :host([open]) .panel { width: calc(100vw - 24px); max-height: calc(100vh - 24px); } }
22618
22774
  </style>`;
22619
22775
 
22776
+ // src/host-appearance.ts
22777
+ var ROLE_VARIABLES = {
22778
+ canvas: "--ns-assistant-canvas",
22779
+ text: "--ns-assistant-text",
22780
+ mutedText: "--ns-assistant-muted-text",
22781
+ link: "--ns-assistant-link",
22782
+ focus: "--ns-assistant-focus",
22783
+ success: "--ns-assistant-success",
22784
+ warning: "--ns-assistant-warning",
22785
+ danger: "--ns-assistant-danger"
22786
+ };
22787
+ var SURFACE_VARIABLES = {
22788
+ panel: "--ns-assistant-panel",
22789
+ header: "--ns-assistant-header",
22790
+ assistantMessage: "--ns-assistant-assistant-message",
22791
+ userMessage: "--ns-assistant-user-message",
22792
+ composer: "--ns-assistant-composer",
22793
+ suggestion: "--ns-assistant-suggestion",
22794
+ confirmation: "--ns-assistant-confirmation",
22795
+ primaryButton: "--ns-assistant-primary-button",
22796
+ secondaryButton: "--ns-assistant-secondary-button",
22797
+ launcher: "--ns-assistant-launcher",
22798
+ code: "--ns-assistant-code",
22799
+ app: "--ns-assistant-app"
22800
+ };
22801
+ var ASSISTANT_APPEARANCE_VARIABLES = [
22802
+ ...Object.values(ROLE_VARIABLES),
22803
+ ...Object.values(SURFACE_VARIABLES).flatMap((name) => [name, `${name}-text`, `${name}-border`])
22804
+ ];
22805
+ function applyHostAppearance(style, appearance, theme, previous = /* @__PURE__ */ new Map()) {
22806
+ for (const [variable, original] of previous) {
22807
+ if (original.value) style.setProperty(variable, original.value, original.priority);
22808
+ else style.removeProperty(variable);
22809
+ }
22810
+ previous.clear();
22811
+ const selected = appearance?.[theme];
22812
+ if (!selected) return [];
22813
+ for (const [role, variable] of Object.entries(ROLE_VARIABLES)) {
22814
+ const value = selected[role];
22815
+ if (typeof value === "string") setManagedProperty(style, previous, variable, value);
22816
+ }
22817
+ for (const [role, variable] of Object.entries(SURFACE_VARIABLES)) {
22818
+ const value = selected[role];
22819
+ if (!value || typeof value !== "object") continue;
22820
+ if (value.surface) setManagedProperty(style, previous, variable, value.surface);
22821
+ if (value.text) setManagedProperty(style, previous, `${variable}-text`, value.text);
22822
+ if (value.border) setManagedProperty(style, previous, `${variable}-border`, value.border);
22823
+ }
22824
+ return contrastWarnings(selected, theme);
22825
+ }
22826
+ function setManagedProperty(style, previous, variable, value) {
22827
+ previous.set(variable, {
22828
+ value: style.getPropertyValue(variable),
22829
+ priority: style.getPropertyPriority(variable)
22830
+ });
22831
+ style.setProperty(variable, value);
22832
+ }
22833
+ function contrastWarnings(theme, mode) {
22834
+ const warnings = [];
22835
+ const pairs = [
22836
+ ["panel", theme.panel?.text ?? theme.text, theme.panel?.surface ?? theme.canvas],
22837
+ ["userMessage", theme.userMessage?.text ?? theme.text, theme.userMessage?.surface],
22838
+ ["composer", theme.composer?.text ?? theme.text, theme.composer?.surface],
22839
+ ["confirmation", theme.confirmation?.text ?? theme.text, theme.confirmation?.surface],
22840
+ ["primaryButton", theme.primaryButton?.text, theme.primaryButton?.surface]
22841
+ ];
22842
+ for (const [role, foreground, background] of pairs) {
22843
+ if (!foreground || !background) continue;
22844
+ const ratio = contrastRatio2(foreground, background);
22845
+ if (ratio >= 4.5) continue;
22846
+ warnings.push({
22847
+ code: "low_contrast",
22848
+ theme: mode,
22849
+ role,
22850
+ foreground,
22851
+ background,
22852
+ contrastRatio: Math.round(ratio * 100) / 100,
22853
+ recommendedRatio: 4.5
22854
+ });
22855
+ }
22856
+ return warnings;
22857
+ }
22858
+ function contrastRatio2(first, second) {
22859
+ const lighter = Math.max(luminance(first), luminance(second));
22860
+ const darker = Math.min(luminance(first), luminance(second));
22861
+ return (lighter + 0.05) / (darker + 0.05);
22862
+ }
22863
+ function luminance(color) {
22864
+ const match = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(color);
22865
+ if (!match) return 1;
22866
+ const values = match.slice(1).map((value) => {
22867
+ const channel = Number.parseInt(value, 16) / 255;
22868
+ return channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4;
22869
+ });
22870
+ return 0.2126 * (values[0] ?? 0) + 0.7152 * (values[1] ?? 0) + 0.0722 * (values[2] ?? 0);
22871
+ }
22872
+
22620
22873
  // src/presentation.ts
22621
22874
  function syncAssistantPresentation(host, root, appearance, mode, sessionState) {
22622
22875
  const presentation = appearance.presentation;
@@ -22789,6 +23042,35 @@ function replaceFallback(root, slotName, nodes) {
22789
23042
  slot?.replaceChildren(...nodes);
22790
23043
  }
22791
23044
 
23045
+ // src/element-theme.ts
23046
+ function applyAssistantElementTheme(input) {
23047
+ const { host, root, appearance, mode } = input;
23048
+ host.setAttribute("data-theme", mode);
23049
+ for (const [name, value] of Object.entries(appearance.theme[mode])) {
23050
+ host.style.setProperty(`--ns-assistant-default-${camelToKebab(name)}`, tokenValue(name, value));
23051
+ }
23052
+ const warnings = applyHostAppearance(host.style, input.hostAppearance, mode, input.styleSnapshot);
23053
+ host.style.setProperty("--ns-assistant-default-panel-width", `${appearance.layout.panelWidth}px`);
23054
+ host.style.setProperty(
23055
+ "--ns-assistant-default-min-height",
23056
+ `${appearance.layout.panelMinHeight}px`
23057
+ );
23058
+ host.style.setProperty(
23059
+ "--ns-assistant-default-max-height",
23060
+ `${appearance.layout.panelMaxHeight}px`
23061
+ );
23062
+ host.style.setProperty("--ns-assistant-default-z-index", String(appearance.layout.zIndex));
23063
+ host.style.setProperty("--ns-assistant-default-edge-offset", `${appearance.layout.edgeOffset}px`);
23064
+ const logo = root?.querySelector(".brand-logo");
23065
+ const logoUrl = appearance.brand.logo[mode];
23066
+ if (logo) {
23067
+ logo.hidden = !logoUrl;
23068
+ if (logoUrl) logo.src = logoUrl;
23069
+ }
23070
+ if (root) syncAssistantPresentation(host, root, appearance, mode, input.sessionState);
23071
+ return warnings;
23072
+ }
23073
+
22792
23074
  // src/presentation-styles.ts
22793
23075
  var presentationStyles = `<style>
22794
23076
  :host([data-mode="floating"]) {
@@ -22994,7 +23276,18 @@ var NoodleAssistantElement = class extends HTMLElementBase {
22994
23276
  #pendingUserEcho;
22995
23277
  #sessionState = "idle";
22996
23278
  #startOpenApplied = false;
23279
+ #hostAppearance;
23280
+ #appearanceWarningKeys = /* @__PURE__ */ new Set();
23281
+ #appearanceStyleSnapshot = /* @__PURE__ */ new Map();
22997
23282
  sessionEndpoint = "";
23283
+ get appearance() {
23284
+ return this.#hostAppearance;
23285
+ }
23286
+ set appearance(value) {
23287
+ this.#hostAppearance = value;
23288
+ this.#appearanceWarningKeys.clear();
23289
+ if (this.isConnected) this.#applyTheme();
23290
+ }
22998
23291
  #events = new AssistantElementEventController({
22999
23292
  element: this,
23000
23293
  messages: () => this.#messages,
@@ -23010,10 +23303,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
23010
23303
  this.#syncAppearance();
23011
23304
  this.#applyStartOpenPolicy();
23012
23305
  },
23013
- labels: () => ({
23014
- confirm: this.#appearance.labels.confirm,
23015
- cancel: this.#appearance.labels.cancel
23016
- }),
23306
+ labels: () => this.#appearance.labels,
23017
23307
  respond: (id, response) => this.respond(id, response),
23018
23308
  dispatchError: (detail) => this.#dispatchError(detail),
23019
23309
  renderView: (detail) => {
@@ -23074,6 +23364,15 @@ var NoodleAssistantElement = class extends HTMLElementBase {
23074
23364
  focusComposer() {
23075
23365
  this.shadowRoot?.querySelector("textarea")?.focus();
23076
23366
  }
23367
+ /** Stop the active stream without discarding the current composer draft or session. */
23368
+ stop() {
23369
+ if (!this.hasAttribute("busy")) return;
23370
+ this.#conversationGeneration += 1;
23371
+ this.#client?.abort();
23372
+ this.#setBusy(false);
23373
+ this.#appendStatus(this.#appearance.labels.stopped);
23374
+ this.dispatchEvent(new CustomEvent("assistant-stopped"));
23375
+ }
23077
23376
  resetSession() {
23078
23377
  this.#conversationGeneration += 1;
23079
23378
  this.#client?.abort();
@@ -23088,6 +23387,20 @@ var NoodleAssistantElement = class extends HTMLElementBase {
23088
23387
  this.#setBusy(false);
23089
23388
  this.#setSessionState("idle");
23090
23389
  }
23390
+ /** Re-establish the session without replaying a message or interaction decision. */
23391
+ reconnect() {
23392
+ this.#conversationGeneration += 1;
23393
+ this.#client?.abort();
23394
+ this.#unsubscribeClient?.();
23395
+ this.#client = void 0;
23396
+ this.#clientEndpoint = "";
23397
+ this.#unsubscribeClient = void 0;
23398
+ this.#sessionBootstrap = void 0;
23399
+ this.#setBusy(false);
23400
+ this.#setSessionState("loading");
23401
+ this.#primeSession();
23402
+ this.dispatchEvent(new CustomEvent("assistant-reconnecting"));
23403
+ }
23091
23404
  updateContext(context) {
23092
23405
  this.#context = { ...context };
23093
23406
  if (this.#client) {
@@ -23135,6 +23448,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
23135
23448
  await this.#ensureClient().sendMessage(message);
23136
23449
  } catch (error51) {
23137
23450
  if (generation !== this.#conversationGeneration) return;
23451
+ if (error51 instanceof AssistantClientError && error51.detail.code === "request_aborted") return;
23138
23452
  this.#dispatchClientError(error51, "turn_failed");
23139
23453
  throw error51;
23140
23454
  } finally {
@@ -23214,26 +23528,31 @@ var NoodleAssistantElement = class extends HTMLElementBase {
23214
23528
  #handleClientEvent(event) {
23215
23529
  this.dispatchEvent(new CustomEvent("assistant-event", { detail: event }));
23216
23530
  this.#events.handle(event);
23217
- if (event.event === "session_started") this.#setSessionState("ready");
23531
+ if (event.event === "session_started") {
23532
+ this.#setSessionState("ready");
23533
+ this.#messages?.querySelector(".conversation-error")?.remove();
23534
+ }
23218
23535
  if (event.event === "session_expired" || event.event === "session_reset") {
23219
23536
  this.#setSessionState("idle");
23220
23537
  }
23221
23538
  if (event.event === "tool_proposed" || event.event === "interaction_proposed" || event.event === "input_requested") {
23222
23539
  this.#removeThinking();
23223
23540
  this.setAttribute("has-messages", "");
23224
- this.#scrollToBottom();
23225
23541
  }
23226
23542
  if (event.event === "content" || event.event === "message_completed" || event.event === "interaction_completed" || event.event === "error") {
23227
23543
  this.#removeThinking();
23228
- this.#scrollToBottom();
23229
23544
  }
23230
23545
  }
23231
23546
  #setBusy(busy) {
23232
23547
  this.toggleAttribute("busy", busy);
23233
23548
  const textarea = this.shadowRoot?.querySelector("textarea");
23234
23549
  const send = this.shadowRoot?.querySelector(".send");
23235
- if (textarea) textarea.disabled = busy;
23236
- if (send) send.disabled = busy;
23550
+ if (textarea) textarea.disabled = false;
23551
+ if (send) {
23552
+ send.dataset.action = busy ? "stop" : "send";
23553
+ send.textContent = busy ? this.#appearance.labels.stop : this.#appearance.labels.send;
23554
+ send.setAttribute("aria-label", send.textContent);
23555
+ }
23237
23556
  if (!busy) {
23238
23557
  this.#removeThinking();
23239
23558
  return;
@@ -23271,6 +23590,16 @@ var NoodleAssistantElement = class extends HTMLElementBase {
23271
23590
  body.textContent = text2;
23272
23591
  }
23273
23592
  message.append(body);
23593
+ if (role === "assistant" && text2) {
23594
+ const copy = document.createElement("button");
23595
+ copy.type = "button";
23596
+ copy.className = "message-copy";
23597
+ copy.textContent = this.#appearance.labels.copy;
23598
+ copy.addEventListener("click", () => {
23599
+ void navigator.clipboard?.writeText(text2);
23600
+ });
23601
+ message.append(copy);
23602
+ }
23274
23603
  if (this.#appearance.behavior.showTimestamps) {
23275
23604
  const timestamp = document.createElement("time");
23276
23605
  timestamp.dateTime = (/* @__PURE__ */ new Date()).toISOString();
@@ -23282,12 +23611,31 @@ var NoodleAssistantElement = class extends HTMLElementBase {
23282
23611
  }
23283
23612
  this.#messages.append(message);
23284
23613
  this.setAttribute("has-messages", "");
23285
- this.#scrollToBottom();
23614
+ this.#revealLatest();
23286
23615
  return body;
23287
23616
  }
23617
+ #appendStatus(text2) {
23618
+ if (!this.#messages) return;
23619
+ const status = document.createElement("p");
23620
+ status.className = "conversation-status";
23621
+ status.setAttribute("role", "status");
23622
+ status.textContent = text2;
23623
+ this.#messages.append(status);
23624
+ }
23288
23625
  #scrollToBottom() {
23289
23626
  if (this.#messages) this.#messages.scrollTop = this.#messages.scrollHeight;
23290
23627
  }
23628
+ #revealLatest() {
23629
+ if (!this.#messages) return;
23630
+ const distance = this.#messages.scrollHeight - this.#messages.scrollTop - this.#messages.clientHeight;
23631
+ const control = this.shadowRoot?.querySelector(".new-messages");
23632
+ if (distance <= 160) {
23633
+ this.#scrollToBottom();
23634
+ if (control) control.hidden = true;
23635
+ } else if (control) {
23636
+ control.hidden = false;
23637
+ }
23638
+ }
23291
23639
  #dispatchError(detail) {
23292
23640
  this.dispatchEvent(new CustomEvent("assistant-error", { detail }));
23293
23641
  }
@@ -23295,9 +23643,27 @@ var NoodleAssistantElement = class extends HTMLElementBase {
23295
23643
  if (error51 instanceof AssistantClientError && error51.detail.code === "session_failed") {
23296
23644
  this.#setSessionState("error");
23297
23645
  }
23298
- this.#dispatchError(
23299
- error51 instanceof AssistantClientError ? error51.detail : { code: fallbackCode, retryable: false }
23646
+ const detail = error51 instanceof AssistantClientError ? error51.detail : { code: fallbackCode, retryable: false };
23647
+ this.#appendRecoveryError(
23648
+ detail.code === "session_expired" ? this.#appearance.labels.sessionExpired : this.#appearance.labels.unavailable
23300
23649
  );
23650
+ this.#dispatchError(detail);
23651
+ }
23652
+ #appendRecoveryError(message) {
23653
+ if (!this.#messages) return;
23654
+ this.#messages.querySelector(".conversation-error")?.remove();
23655
+ const error51 = document.createElement("section");
23656
+ error51.className = "conversation-error";
23657
+ error51.setAttribute("role", "alert");
23658
+ const text2 = document.createElement("p");
23659
+ text2.textContent = message;
23660
+ const retry = document.createElement("button");
23661
+ retry.type = "button";
23662
+ retry.textContent = this.#appearance.labels.reconnect;
23663
+ retry.addEventListener("click", () => this.reconnect());
23664
+ error51.append(text2, retry);
23665
+ this.#messages.append(error51);
23666
+ this.#revealLatest();
23301
23667
  }
23302
23668
  #resolvedMode() {
23303
23669
  const configured = this.getAttribute("theme") ?? this.#appearance.theme.defaultMode;
@@ -23305,43 +23671,24 @@ var NoodleAssistantElement = class extends HTMLElementBase {
23305
23671
  }
23306
23672
  #applyTheme() {
23307
23673
  const mode = this.#resolvedMode();
23308
- this.setAttribute("data-theme", mode);
23309
- const tokens = this.#appearance.theme[mode];
23310
- for (const [name, value] of Object.entries(tokens)) {
23311
- this.style.setProperty(
23312
- `--ns-assistant-default-${camelToKebab(name)}`,
23313
- tokenValue(name, value)
23674
+ for (const warning of applyAssistantElementTheme({
23675
+ host: this,
23676
+ root: this.shadowRoot,
23677
+ appearance: this.#appearance,
23678
+ hostAppearance: this.#hostAppearance,
23679
+ mode,
23680
+ sessionState: this.#sessionState,
23681
+ styleSnapshot: this.#appearanceStyleSnapshot
23682
+ })) {
23683
+ const key = `${warning.theme}:${warning.role}:${warning.foreground}:${warning.background}`;
23684
+ if (this.#appearanceWarningKeys.has(key)) continue;
23685
+ this.#appearanceWarningKeys.add(key);
23686
+ this.dispatchEvent(
23687
+ new CustomEvent("assistant-appearance-warning", {
23688
+ detail: warning
23689
+ })
23314
23690
  );
23315
23691
  }
23316
- this.style.setProperty(
23317
- "--ns-assistant-default-panel-width",
23318
- `${this.#appearance.layout.panelWidth}px`
23319
- );
23320
- this.style.setProperty(
23321
- "--ns-assistant-default-min-height",
23322
- `${this.#appearance.layout.panelMinHeight}px`
23323
- );
23324
- this.style.setProperty(
23325
- "--ns-assistant-default-max-height",
23326
- `${this.#appearance.layout.panelMaxHeight}px`
23327
- );
23328
- this.style.setProperty(
23329
- "--ns-assistant-default-z-index",
23330
- String(this.#appearance.layout.zIndex)
23331
- );
23332
- this.style.setProperty(
23333
- "--ns-assistant-default-edge-offset",
23334
- `${this.#appearance.layout.edgeOffset}px`
23335
- );
23336
- const logo = this.shadowRoot?.querySelector(".brand-logo");
23337
- const logoUrl = this.#appearance.brand.logo[mode];
23338
- if (logo) {
23339
- logo.hidden = !logoUrl;
23340
- if (logoUrl) logo.src = logoUrl;
23341
- }
23342
- if (this.shadowRoot) {
23343
- syncAssistantPresentation(this, this.shadowRoot, this.#appearance, mode, this.#sessionState);
23344
- }
23345
23692
  }
23346
23693
  #setSessionState(state) {
23347
23694
  this.#sessionState = state;
@@ -23421,6 +23768,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
23421
23768
  <div class="welcome"><slot name="empty-state"></slot></div>
23422
23769
  <div class="suggested-prompts"></div>
23423
23770
  <div class="messages" aria-live="polite"></div>
23771
+ <button class="new-messages" type="button" hidden></button>
23424
23772
  <form><slot name="composer-leading"></slot><textarea rows="1"></textarea><button class="send" type="submit"></button><slot name="composer-trailing"></slot></form>
23425
23773
  <nav class="legal" aria-label="Legal"></nav>
23426
23774
  <slot name="conversation-footer"></slot>
@@ -23463,6 +23811,10 @@ var NoodleAssistantElement = class extends HTMLElementBase {
23463
23811
  const form = queryRequired(this.shadowRoot, "form");
23464
23812
  form.addEventListener("submit", (event) => {
23465
23813
  event.preventDefault();
23814
+ if (this.hasAttribute("busy")) {
23815
+ this.stop();
23816
+ return;
23817
+ }
23466
23818
  const value = textarea.value;
23467
23819
  textarea.value = "";
23468
23820
  textarea.style.height = "auto";
@@ -23477,6 +23829,17 @@ var NoodleAssistantElement = class extends HTMLElementBase {
23477
23829
  event.preventDefault();
23478
23830
  form.requestSubmit();
23479
23831
  });
23832
+ const newMessages = queryRequired(this.shadowRoot, ".new-messages");
23833
+ newMessages.textContent = appearance.labels.newMessages;
23834
+ newMessages.addEventListener("click", () => {
23835
+ this.#scrollToBottom();
23836
+ newMessages.hidden = true;
23837
+ });
23838
+ this.#messages?.addEventListener("scroll", () => {
23839
+ if (this.#messages && this.#messages.scrollHeight - this.#messages.scrollTop - this.#messages.clientHeight < 48) {
23840
+ newMessages.hidden = true;
23841
+ }
23842
+ });
23480
23843
  this.#applyTheme();
23481
23844
  }
23482
23845
  #appendLegalLinks(legal) {
@@ -23517,12 +23880,15 @@ var NoodleAssistantWithRef = (0, import_react.forwardRef)(
23517
23880
  const error51 = (event) => props.onError?.(event.detail);
23518
23881
  const expired = () => props.onSessionExpired?.();
23519
23882
  const assistantEvent = (event) => props.onEvent?.(event.detail);
23883
+ const appearanceWarning = (event) => props.onAppearanceWarning?.(event.detail);
23520
23884
  element.addEventListener("assistant-ready", ready);
23521
23885
  element.addEventListener("assistant-error", error51);
23522
23886
  element.addEventListener("assistant-session-expired", expired);
23523
23887
  element.addEventListener("assistant-event", assistantEvent);
23888
+ element.addEventListener("assistant-appearance-warning", appearanceWarning);
23524
23889
  registerNoodleAssistant();
23525
23890
  element.sessionEndpoint = props.sessionEndpoint;
23891
+ element.appearance = props.appearance;
23526
23892
  if (props.context) element.updateContext(props.context);
23527
23893
  if (props.pageContext) element.updatePageContext(props.pageContext);
23528
23894
  if (props.modelContext) element.updateModelContext(props.modelContext);
@@ -23532,11 +23898,14 @@ var NoodleAssistantWithRef = (0, import_react.forwardRef)(
23532
23898
  element.removeEventListener("assistant-error", error51);
23533
23899
  element.removeEventListener("assistant-session-expired", expired);
23534
23900
  element.removeEventListener("assistant-event", assistantEvent);
23901
+ element.removeEventListener("assistant-appearance-warning", appearanceWarning);
23535
23902
  };
23536
23903
  }, [
23537
23904
  props.context,
23905
+ props.appearance,
23538
23906
  props.modelContext,
23539
23907
  props.onError,
23908
+ props.onAppearanceWarning,
23540
23909
  props.onEvent,
23541
23910
  props.onReady,
23542
23911
  props.onSessionExpired,