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