@noodleseed/assistant 1.4.0 → 1.5.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.
@@ -2,7 +2,7 @@ import {
2
2
  AssistantClientError,
3
3
  copyAssistantModelContext,
4
4
  createAssistantClient
5
- } from "./chunk-P34VLBIK.js";
5
+ } from "./chunk-GTYYQNGT.js";
6
6
  import {
7
7
  _enum,
8
8
  _null,
@@ -3158,12 +3158,12 @@ var common = {
3158
3158
  baseFontSize: 15,
3159
3159
  messageFontSize: 15,
3160
3160
  lineHeight: 1.5,
3161
- panelRadius: 24,
3162
- cardRadius: 16,
3163
- inputRadius: 999,
3164
- buttonRadius: 999,
3161
+ panelRadius: 20,
3162
+ cardRadius: 14,
3163
+ inputRadius: 14,
3164
+ buttonRadius: 12,
3165
3165
  launcherRadius: 999,
3166
- spacing: 12,
3166
+ spacing: 8,
3167
3167
  backdropBlur: 18,
3168
3168
  motionScale: 1
3169
3169
  };
@@ -3221,7 +3221,7 @@ var DEFAULT_APPEARANCE = {
3221
3221
  layout: {
3222
3222
  mode: "floating",
3223
3223
  position: "bottom-right",
3224
- panelWidth: 560,
3224
+ panelWidth: 520,
3225
3225
  panelMinHeight: 540,
3226
3226
  panelMaxHeight: 820,
3227
3227
  edgeOffset: 20,
@@ -3248,7 +3248,16 @@ var DEFAULT_APPEARANCE = {
3248
3248
  close: "Close assistant",
3249
3249
  open: "Open assistant",
3250
3250
  confirm: "Confirm",
3251
+ decline: "Don't proceed",
3251
3252
  cancel: "Cancel",
3253
+ confirmationHeading: "Review and confirm",
3254
+ additionalDetails: "Additional details",
3255
+ redacted: "Hidden for security",
3256
+ completed: "Completed",
3257
+ stopped: "Stopped",
3258
+ copy: "Copy",
3259
+ newMessages: "New messages",
3260
+ reconnect: "Reconnect",
3252
3261
  retry: "Try again",
3253
3262
  unavailable: "The assistant is temporarily unavailable.",
3254
3263
  sessionExpired: "Your assistant session expired. Reopen it to continue.",
@@ -3259,7 +3268,7 @@ var DEFAULT_APPEARANCE = {
3259
3268
  },
3260
3269
  presentation: {
3261
3270
  panel: {
3262
- surface: "glass",
3271
+ surface: "solid",
3263
3272
  elevation: "soft",
3264
3273
  border: "subtle"
3265
3274
  },
@@ -3707,22 +3716,54 @@ function record2(value) {
3707
3716
  function createInteractionCard(options) {
3708
3717
  const card = document.createElement("section");
3709
3718
  card.className = "tool-proposal";
3710
- const label = document.createElement("strong");
3711
- label.textContent = options.tool;
3712
- card.append(label);
3719
+ card.setAttribute("aria-label", options.labels.heading);
3720
+ const eyebrow = document.createElement("span");
3721
+ eyebrow.className = "proposal-eyebrow";
3722
+ eyebrow.textContent = options.labels.heading;
3723
+ const title = document.createElement("h3");
3724
+ title.textContent = options.title ?? humanizeArgumentName(options.tool);
3725
+ card.append(eyebrow, title);
3726
+ if (options.description) {
3727
+ const description = document.createElement("p");
3728
+ description.className = "proposal-description";
3729
+ description.textContent = options.description;
3730
+ card.append(description);
3731
+ }
3713
3732
  if (options.arguments && Object.keys(options.arguments).length > 0) {
3714
- card.append(createArgumentList(options.arguments));
3733
+ const { action, ...businessArguments } = options.arguments;
3734
+ if (Object.keys(businessArguments).length > 0) {
3735
+ card.append(createArgumentList(businessArguments, options.reviewSchema, options.labels));
3736
+ }
3737
+ if (action !== void 0) {
3738
+ const technical = document.createElement("details");
3739
+ technical.className = "proposal-details";
3740
+ const summary = document.createElement("summary");
3741
+ summary.textContent = options.labels.details;
3742
+ const properties = record3(options.reviewSchema?.properties);
3743
+ technical.append(summary);
3744
+ const actionObject = record3(action);
3745
+ if (actionObject) {
3746
+ technical.append(
3747
+ createArgumentList(actionObject, record3(properties?.action), options.labels)
3748
+ );
3749
+ } else {
3750
+ const value = document.createElement("div");
3751
+ appendValue(value, action, record3(properties?.action), options.labels);
3752
+ technical.append(value);
3753
+ }
3754
+ card.append(technical);
3755
+ }
3715
3756
  }
3716
3757
  const actions = document.createElement("div");
3717
3758
  actions.className = "proposal-actions";
3718
3759
  const buttons = [];
3719
3760
  for (const [labelText, action] of [
3720
3761
  [options.labels.accept, "accept"],
3721
- [options.labels.decline, "decline"],
3722
- [options.labels.cancel, "cancel"]
3762
+ [options.labels.decline, "decline"]
3723
3763
  ]) {
3724
3764
  const button2 = document.createElement("button");
3725
3765
  button2.type = "button";
3766
+ button2.className = `proposal-${action}`;
3726
3767
  button2.textContent = labelText;
3727
3768
  buttons.push(button2);
3728
3769
  button2.addEventListener("click", () => {
@@ -3736,27 +3777,98 @@ function createInteractionCard(options) {
3736
3777
  card.append(actions);
3737
3778
  return card;
3738
3779
  }
3739
- function createArgumentList(arguments_) {
3740
- const details = document.createElement("dl");
3741
- details.className = "proposal-arguments";
3780
+ function createResultCard(result, label, redactedLabel) {
3781
+ const card = document.createElement("section");
3782
+ card.className = "tool-result";
3783
+ const heading = document.createElement("strong");
3784
+ heading.textContent = label;
3785
+ card.append(heading);
3786
+ const body = document.createElement("div");
3787
+ const labels = {
3788
+ heading: label,
3789
+ accept: "",
3790
+ decline: "",
3791
+ details: "",
3792
+ redacted: redactedLabel
3793
+ };
3794
+ appendValue(body, result, void 0, labels);
3795
+ card.append(body);
3796
+ return card;
3797
+ }
3798
+ function createArgumentList(arguments_, schema, labels) {
3799
+ const list = document.createElement("dl");
3800
+ list.className = "proposal-arguments";
3801
+ const properties = record3(schema?.properties);
3742
3802
  for (const [name, value] of Object.entries(arguments_)) {
3803
+ const propertySchema = record3(properties?.[name]);
3743
3804
  const term = document.createElement("dt");
3744
- term.textContent = humanizeArgumentName(name);
3805
+ term.textContent = string2(propertySchema?.title) ?? humanizeArgumentName(name);
3806
+ const help = string2(propertySchema?.description);
3807
+ if (help) {
3808
+ const description2 = document.createElement("small");
3809
+ description2.textContent = help;
3810
+ term.append(description2);
3811
+ }
3745
3812
  const description = document.createElement("dd");
3746
- description.textContent = formatArgumentValue(value);
3747
- details.append(term, description);
3813
+ appendValue(description, value, propertySchema, labels);
3814
+ list.append(term, description);
3815
+ }
3816
+ return list;
3817
+ }
3818
+ function appendValue(parent, value, schema, labels) {
3819
+ if (value === "[REDACTED]") {
3820
+ const hidden = document.createElement("span");
3821
+ hidden.className = "proposal-redacted";
3822
+ hidden.textContent = labels.redacted;
3823
+ parent.append(hidden);
3824
+ return;
3825
+ }
3826
+ if (Array.isArray(value)) {
3827
+ const list = document.createElement("ul");
3828
+ const itemSchema = record3(schema?.items);
3829
+ for (const item of value) {
3830
+ const entry = document.createElement("li");
3831
+ appendValue(entry, item, itemSchema, labels);
3832
+ list.append(entry);
3833
+ }
3834
+ parent.append(list);
3835
+ return;
3836
+ }
3837
+ const objectValue = record3(value);
3838
+ if (objectValue) {
3839
+ parent.append(createArgumentList(objectValue, schema, labels));
3840
+ return;
3841
+ }
3842
+ parent.textContent = formatScalar(value, schema);
3843
+ }
3844
+ function formatScalar(value, schema) {
3845
+ if (value === null || value === void 0 || value === "") return "Not provided";
3846
+ if (typeof value === "boolean") return value ? "Yes" : "No";
3847
+ if (typeof value === "number") return new Intl.NumberFormat().format(value);
3848
+ if (typeof value !== "string") return String(value);
3849
+ if (schema?.format === "date") {
3850
+ const date = /* @__PURE__ */ new Date(`${value}T00:00:00`);
3851
+ if (!Number.isNaN(date.valueOf()))
3852
+ return new Intl.DateTimeFormat(void 0, { dateStyle: "medium" }).format(date);
3853
+ }
3854
+ if (schema?.format === "date-time") {
3855
+ const date = new Date(value);
3856
+ if (!Number.isNaN(date.valueOf()))
3857
+ return new Intl.DateTimeFormat(void 0, { dateStyle: "medium", timeStyle: "short" }).format(
3858
+ date
3859
+ );
3748
3860
  }
3749
- return details;
3861
+ return value;
3750
3862
  }
3751
3863
  function humanizeArgumentName(value) {
3752
3864
  const words = value.replaceAll(/([a-z0-9])([A-Z])/g, "$1 $2").replaceAll(/[_-]+/g, " ");
3753
3865
  return words ? `${words[0]?.toUpperCase() ?? ""}${words.slice(1)}` : value;
3754
3866
  }
3755
- function formatArgumentValue(value) {
3756
- if (value === null) return "None";
3757
- if (typeof value === "string") return value;
3758
- if (typeof value === "number" || typeof value === "boolean") return String(value);
3759
- return JSON.stringify(value) ?? "";
3867
+ function record3(value) {
3868
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3869
+ }
3870
+ function string2(value) {
3871
+ return typeof value === "string" && value.length > 0 ? value : void 0;
3760
3872
  }
3761
3873
 
3762
3874
  // ../../node_modules/.pnpm/dompurify@3.4.11/node_modules/dompurify/dist/purify.es.mjs
@@ -6492,8 +6604,11 @@ var AssistantElementEventController = class {
6492
6604
  if (!this.#streamKind) return;
6493
6605
  this.#appendToolProposal(
6494
6606
  String(data.id ?? ""),
6495
- String(data.tool ?? ("title" in data ? data.title : void 0) ?? "Action"),
6496
- isRecord(data.arguments) ? data.arguments : void 0
6607
+ String(data.tool ?? "action"),
6608
+ isRecord(data.arguments) ? data.arguments : void 0,
6609
+ typeof data.title === "string" ? data.title : void 0,
6610
+ "description" in data && typeof data.description === "string" ? data.description : void 0,
6611
+ "reviewSchema" in data && isRecord(data.reviewSchema) ? data.reviewSchema : void 0
6497
6612
  );
6498
6613
  return;
6499
6614
  }
@@ -6531,7 +6646,13 @@ var AssistantElementEventController = class {
6531
6646
  }
6532
6647
  #finishInteraction(id, action) {
6533
6648
  if (this.#completedTool && !this.#assistantBody) {
6534
- this.#host.appendMessage("assistant", JSON.stringify(this.#completedTool.result ?? {}) ?? "");
6649
+ const messages = this.#host.messages();
6650
+ if (messages) {
6651
+ const labels = this.#host.labels();
6652
+ messages.append(
6653
+ createResultCard(this.#completedTool.result, labels.completed, labels.redacted)
6654
+ );
6655
+ }
6535
6656
  }
6536
6657
  this.#proposalCards.get(id)?.remove();
6537
6658
  this.#proposalCards.delete(id);
@@ -6546,18 +6667,23 @@ var AssistantElementEventController = class {
6546
6667
  this.#clearStream();
6547
6668
  this.#completedTool = void 0;
6548
6669
  }
6549
- #appendToolProposal(id, tool, arguments_) {
6670
+ #appendToolProposal(id, tool, arguments_, title, description, reviewSchema) {
6550
6671
  const messages = this.#host.messages();
6551
6672
  if (!messages || !id) return;
6552
6673
  this.#proposalCards.get(id)?.remove();
6553
6674
  const labels = this.#host.labels();
6554
6675
  const card = createInteractionCard({
6555
6676
  tool,
6677
+ ...title ? { title } : {},
6678
+ ...description ? { description } : {},
6556
6679
  ...arguments_ ? { arguments: arguments_ } : {},
6680
+ ...reviewSchema ? { reviewSchema } : {},
6557
6681
  labels: {
6682
+ heading: labels.confirmationHeading,
6558
6683
  accept: labels.confirm,
6559
- decline: "Decline",
6560
- cancel: labels.cancel
6684
+ decline: labels.decline,
6685
+ details: labels.additionalDetails,
6686
+ redacted: labels.redacted
6561
6687
  },
6562
6688
  respond: (action) => this.#host.respond(id, { action })
6563
6689
  });
@@ -6565,7 +6691,9 @@ var AssistantElementEventController = class {
6565
6691
  this.#proposalCards.set(id, card);
6566
6692
  this.#revealLatestMessage(messages);
6567
6693
  this.#host.element.dispatchEvent(
6568
- new CustomEvent("assistant-tool-proposed", { detail: { id, tool, arguments: arguments_ } })
6694
+ new CustomEvent("assistant-tool-proposed", {
6695
+ detail: { id, tool, title, description, arguments: arguments_, reviewSchema }
6696
+ })
6569
6697
  );
6570
6698
  }
6571
6699
  #appendInputRequest(id, message, requestedSchema) {
@@ -6616,7 +6744,14 @@ var AssistantElementEventController = class {
6616
6744
  }
6617
6745
  #revealLatestMessage(messages) {
6618
6746
  this.#host.element.setAttribute("has-messages", "");
6619
- messages.scrollTop = messages.scrollHeight;
6747
+ const distance = messages.scrollHeight - messages.scrollTop - messages.clientHeight;
6748
+ const control = this.#host.element.shadowRoot?.querySelector(".new-messages");
6749
+ if (distance <= 160) {
6750
+ messages.scrollTop = messages.scrollHeight;
6751
+ if (control) control.hidden = true;
6752
+ } else if (control) {
6753
+ control.hidden = false;
6754
+ }
6620
6755
  }
6621
6756
  };
6622
6757
  function isRecord(value) {
@@ -6651,17 +6786,18 @@ var ASSISTANT_ELEMENT_STYLES = `<style>
6651
6786
  :host([data-mode="floating"][data-position="bottom-left"]) { right: auto; left: 20px; }
6652
6787
  button { font: inherit; }
6653
6788
  button, textarea, input, select { -webkit-tap-highlight-color: transparent; }
6654
- .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; }
6789
+ .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; }
6655
6790
  .launcher:hover { transform: translateY(-2px) scale(1.03); box-shadow: 0 8px 26px rgba(0,0,0,.16); }
6656
6791
  .launcher img { width: 28px; height: 28px; border-radius: 50%; object-fit: contain; }
6657
- .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; }
6792
+ .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; }
6658
6793
  :host([open]) .panel { display: flex; }
6794
+ .panel { position: relative; }
6659
6795
  :host([open]) .launcher { display: none; }
6660
- 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)); }
6796
+ 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))); }
6661
6797
  header strong { flex: 1; font-size: 16px; font-weight: 600; letter-spacing: -.01em; }
6662
6798
  .brand-logo { display: block; max-width: 120px; max-height: 28px; object-fit: contain; }
6663
6799
  .close, .send { display: grid; place-items: center; border: 0; border-radius: 999px; cursor: pointer; }
6664
- .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; }
6800
+ .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; }
6665
6801
  .close:hover { color: var(--ns-assistant-text, var(--ns-assistant-default-text)); background: var(--ns-assistant-elevated, var(--ns-assistant-default-elevated)); }
6666
6802
  .welcome { flex: 0 0 auto; padding: 30px 22px 18px; }
6667
6803
  .welcome h2 { margin: 0; font-size: 1.42em; line-height: 1.25; letter-spacing: -.025em; }
@@ -6669,16 +6805,24 @@ var ASSISTANT_ELEMENT_STYLES = `<style>
6669
6805
  :host([has-messages]) .welcome, :host([has-messages]) .suggested-prompts { display: none; }
6670
6806
  .suggested-prompts { display: flex; flex: 0 0 auto; flex-wrap: wrap; gap: 8px; padding: 0 20px 16px; }
6671
6807
  .suggested-prompts:empty { display: none; }
6672
- .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; }
6808
+ .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; }
6673
6809
  .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); }
6674
- .messages { flex: 1 1 auto; min-height: 0; padding: 18px 20px; overflow-y: auto; overscroll-behavior: contain; scroll-behavior: smooth; }
6810
+ .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; }
6675
6811
  .messages::-webkit-scrollbar { width: 6px; }
6676
6812
  .messages::-webkit-scrollbar-thumb { background: var(--ns-assistant-divider, var(--ns-assistant-default-divider)); border-radius: 999px; }
6677
6813
  .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; }
6678
6814
  .message > span, .message > .markdown { overflow-wrap: anywhere; }
6679
6815
  .message .avatar { float: left; width: 24px; height: 24px; margin-right: 9px; border-radius: 50%; object-fit: cover; }
6680
6816
  .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; }
6681
- .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; }
6817
+ .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; }
6818
+ .message:hover .message-copy, .message:focus-within .message-copy { opacity: 1; }
6819
+ .conversation-status { margin: 4px auto 14px; color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); font-size: .82em; text-align: center; }
6820
+ .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); }
6821
+ .conversation-error p { margin: 0; }
6822
+ .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; }
6823
+ .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; }
6824
+ .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); }
6825
+ .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; }
6682
6826
  .message.user time { margin-top: 0; }
6683
6827
  .markdown { white-space: normal; }
6684
6828
  .markdown p { margin: 0 0 8px; }
@@ -6687,8 +6831,8 @@ var ASSISTANT_ELEMENT_STYLES = `<style>
6687
6831
  .markdown em, .markdown i { font-style: italic; }
6688
6832
  .markdown a { color: var(--ns-assistant-link, var(--ns-assistant-default-link)); text-decoration: underline; text-underline-offset: 2px; }
6689
6833
  .markdown a:hover { opacity: .8; }
6690
- .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; }
6691
- .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; }
6834
+ .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; }
6835
+ .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; }
6692
6836
  .markdown pre code { padding: 0; background: transparent; font-size: .86em; }
6693
6837
  .markdown ul, .markdown ol { margin: 8px 0; padding-left: 22px; }
6694
6838
  .markdown li { margin: 3px 0; }
@@ -6697,15 +6841,25 @@ var ASSISTANT_ELEMENT_STYLES = `<style>
6697
6841
  .markdown h1:first-child, .markdown h2:first-child, .markdown h3:first-child { margin-top: 0; }
6698
6842
  .markdown h1 { font-size: 1.3em; } .markdown h2 { font-size: 1.18em; } .markdown h3 { font-size: 1.08em; }
6699
6843
  .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; }
6700
- .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; }
6701
- .tool-proposal > strong { font-weight: 650; overflow-wrap: anywhere; }
6702
- .proposal-arguments { display: grid; grid-template-columns: minmax(90px, auto) minmax(0, 1fr); gap: 4px 10px; margin: 0; font-size: .9em; }
6844
+ .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; }
6845
+ .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; }
6846
+ .tool-proposal h3 { margin: -4px 0 0; font-size: 1.08em; line-height: 1.3; letter-spacing: -.01em; }
6847
+ .proposal-description { margin: -4px 0 0; color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); }
6848
+ .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; }
6849
+ .proposal-arguments .proposal-arguments { grid-column: 1 / -1; padding: 4px 0 0; border: 0; }
6703
6850
  .proposal-arguments dt { color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); }
6851
+ .proposal-arguments dt small { display: block; margin-top: 2px; font-size: .85em; line-height: 1.35; }
6704
6852
  .proposal-arguments dd { margin: 0; overflow-wrap: anywhere; }
6853
+ .proposal-details { color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); font-size: .86em; }
6854
+ .proposal-details summary { min-height: 36px; padding: 8px 0; cursor: pointer; font-weight: 600; }
6855
+ .proposal-details .proposal-arguments { margin-top: 4px; }
6705
6856
  .proposal-actions { display: flex; flex-wrap: wrap; gap: 8px; justify-content: end; }
6706
- .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; }
6707
- .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)); }
6857
+ .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; }
6858
+ .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); }
6708
6859
  .tool-proposal button:disabled { opacity: .55; cursor: not-allowed; }
6860
+ .tool-proposal button { min-height: 45px; }
6861
+ .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))); }
6862
+ .tool-result > strong { color: var(--ns-assistant-success, var(--ns-assistant-default-success)); }
6709
6863
  .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; }
6710
6864
  .input-request form:focus-within { box-shadow: none; }
6711
6865
  .input-request label { display: grid; gap: 4px; color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); font-size: .9em; }
@@ -6714,20 +6868,21 @@ var ASSISTANT_ELEMENT_STYLES = `<style>
6714
6868
  .input-request input[type="checkbox"] { width: auto; justify-self: start; accent-color: var(--ns-assistant-accent, var(--ns-assistant-default-accent)); }
6715
6869
  .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)); }
6716
6870
  .input-request .proposal-actions { margin-top: 2px; }
6717
- .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)); }
6871
+ .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))); }
6718
6872
  .noodle-app-title { padding: 10px 12px 0; font-size: .86em; font-weight: 650; }
6719
6873
  .noodle-app-frame { display: block; width: 100%; min-height: 120px; border: 0; background: transparent; }
6720
6874
  .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)); }
6721
6875
  .noodle-app-card[data-fullscreen] .noodle-app-frame { height: 100% !important; }
6722
- 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; }
6876
+ 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; }
6723
6877
  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)); }
6724
6878
  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; }
6725
6879
  textarea::placeholder { color: var(--ns-assistant-muted-text, var(--ns-assistant-default-muted-text)); }
6726
6880
  textarea:focus, textarea:focus-visible { outline: none; box-shadow: none; }
6727
6881
  button:focus-visible { outline: 3px solid var(--ns-assistant-focus, var(--ns-assistant-default-focus)); outline-offset: 2px; }
6728
- .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; }
6729
- .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; }
6882
+ .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; }
6883
+ .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; }
6730
6884
  .send:hover { transform: scale(1.05); background: var(--ns-assistant-hover, var(--ns-assistant-default-hover)); }
6885
+ :host([busy]) .send::before { width: 13px; height: 13px; border-radius: 3px; -webkit-mask: none; mask: none; }
6731
6886
  .send:disabled, textarea:disabled { opacity: .55; cursor: not-allowed; }
6732
6887
  .legal { display: flex; gap: 12px; justify-content: center; padding: 0 12px 9px; font-size: .75em; }
6733
6888
  .legal:empty { display: none; }
@@ -6740,9 +6895,107 @@ var ASSISTANT_ELEMENT_STYLES = `<style>
6740
6895
  @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; } }
6741
6896
  @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; } .messages { scroll-behavior: auto; } }
6742
6897
  @media (forced-colors: active) { .message.thinking > span { color: CanvasText; background: none; } }
6898
+ @media (hover: none) { .message-copy { opacity: 1; } }
6743
6899
  @media (max-width: 560px) { :host([data-mode="floating"]) { inset: auto 12px 12px; } :host([open]) .panel { width: calc(100vw - 24px); max-height: calc(100vh - 24px); } }
6744
6900
  </style>`;
6745
6901
 
6902
+ // src/host-appearance.ts
6903
+ var ROLE_VARIABLES = {
6904
+ canvas: "--ns-assistant-canvas",
6905
+ text: "--ns-assistant-text",
6906
+ mutedText: "--ns-assistant-muted-text",
6907
+ link: "--ns-assistant-link",
6908
+ focus: "--ns-assistant-focus",
6909
+ success: "--ns-assistant-success",
6910
+ warning: "--ns-assistant-warning",
6911
+ danger: "--ns-assistant-danger"
6912
+ };
6913
+ var SURFACE_VARIABLES = {
6914
+ panel: "--ns-assistant-panel",
6915
+ header: "--ns-assistant-header",
6916
+ assistantMessage: "--ns-assistant-assistant-message",
6917
+ userMessage: "--ns-assistant-user-message",
6918
+ composer: "--ns-assistant-composer",
6919
+ suggestion: "--ns-assistant-suggestion",
6920
+ confirmation: "--ns-assistant-confirmation",
6921
+ primaryButton: "--ns-assistant-primary-button",
6922
+ secondaryButton: "--ns-assistant-secondary-button",
6923
+ launcher: "--ns-assistant-launcher",
6924
+ code: "--ns-assistant-code",
6925
+ app: "--ns-assistant-app"
6926
+ };
6927
+ var ASSISTANT_APPEARANCE_VARIABLES = [
6928
+ ...Object.values(ROLE_VARIABLES),
6929
+ ...Object.values(SURFACE_VARIABLES).flatMap((name) => [name, `${name}-text`, `${name}-border`])
6930
+ ];
6931
+ function applyHostAppearance(style, appearance, theme, previous = /* @__PURE__ */ new Map()) {
6932
+ for (const [variable, original] of previous) {
6933
+ if (original.value) style.setProperty(variable, original.value, original.priority);
6934
+ else style.removeProperty(variable);
6935
+ }
6936
+ previous.clear();
6937
+ const selected = appearance?.[theme];
6938
+ if (!selected) return [];
6939
+ for (const [role, variable] of Object.entries(ROLE_VARIABLES)) {
6940
+ const value = selected[role];
6941
+ if (typeof value === "string") setManagedProperty(style, previous, variable, value);
6942
+ }
6943
+ for (const [role, variable] of Object.entries(SURFACE_VARIABLES)) {
6944
+ const value = selected[role];
6945
+ if (!value || typeof value !== "object") continue;
6946
+ if (value.surface) setManagedProperty(style, previous, variable, value.surface);
6947
+ if (value.text) setManagedProperty(style, previous, `${variable}-text`, value.text);
6948
+ if (value.border) setManagedProperty(style, previous, `${variable}-border`, value.border);
6949
+ }
6950
+ return contrastWarnings(selected, theme);
6951
+ }
6952
+ function setManagedProperty(style, previous, variable, value) {
6953
+ previous.set(variable, {
6954
+ value: style.getPropertyValue(variable),
6955
+ priority: style.getPropertyPriority(variable)
6956
+ });
6957
+ style.setProperty(variable, value);
6958
+ }
6959
+ function contrastWarnings(theme, mode) {
6960
+ const warnings = [];
6961
+ const pairs = [
6962
+ ["panel", theme.panel?.text ?? theme.text, theme.panel?.surface ?? theme.canvas],
6963
+ ["userMessage", theme.userMessage?.text ?? theme.text, theme.userMessage?.surface],
6964
+ ["composer", theme.composer?.text ?? theme.text, theme.composer?.surface],
6965
+ ["confirmation", theme.confirmation?.text ?? theme.text, theme.confirmation?.surface],
6966
+ ["primaryButton", theme.primaryButton?.text, theme.primaryButton?.surface]
6967
+ ];
6968
+ for (const [role, foreground, background] of pairs) {
6969
+ if (!foreground || !background) continue;
6970
+ const ratio = contrastRatio2(foreground, background);
6971
+ if (ratio >= 4.5) continue;
6972
+ warnings.push({
6973
+ code: "low_contrast",
6974
+ theme: mode,
6975
+ role,
6976
+ foreground,
6977
+ background,
6978
+ contrastRatio: Math.round(ratio * 100) / 100,
6979
+ recommendedRatio: 4.5
6980
+ });
6981
+ }
6982
+ return warnings;
6983
+ }
6984
+ function contrastRatio2(first, second) {
6985
+ const lighter = Math.max(luminance(first), luminance(second));
6986
+ const darker = Math.min(luminance(first), luminance(second));
6987
+ return (lighter + 0.05) / (darker + 0.05);
6988
+ }
6989
+ function luminance(color) {
6990
+ const match = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(color);
6991
+ if (!match) return 1;
6992
+ const values = match.slice(1).map((value) => {
6993
+ const channel = Number.parseInt(value, 16) / 255;
6994
+ return channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4;
6995
+ });
6996
+ return 0.2126 * (values[0] ?? 0) + 0.7152 * (values[1] ?? 0) + 0.0722 * (values[2] ?? 0);
6997
+ }
6998
+
6746
6999
  // src/presentation.ts
6747
7000
  function syncAssistantPresentation(host, root, appearance, mode, sessionState) {
6748
7001
  const presentation = appearance.presentation;
@@ -6915,6 +7168,35 @@ function replaceFallback(root, slotName, nodes) {
6915
7168
  slot?.replaceChildren(...nodes);
6916
7169
  }
6917
7170
 
7171
+ // src/element-theme.ts
7172
+ function applyAssistantElementTheme(input) {
7173
+ const { host, root, appearance, mode } = input;
7174
+ host.setAttribute("data-theme", mode);
7175
+ for (const [name, value] of Object.entries(appearance.theme[mode])) {
7176
+ host.style.setProperty(`--ns-assistant-default-${camelToKebab(name)}`, tokenValue(name, value));
7177
+ }
7178
+ const warnings = applyHostAppearance(host.style, input.hostAppearance, mode, input.styleSnapshot);
7179
+ host.style.setProperty("--ns-assistant-default-panel-width", `${appearance.layout.panelWidth}px`);
7180
+ host.style.setProperty(
7181
+ "--ns-assistant-default-min-height",
7182
+ `${appearance.layout.panelMinHeight}px`
7183
+ );
7184
+ host.style.setProperty(
7185
+ "--ns-assistant-default-max-height",
7186
+ `${appearance.layout.panelMaxHeight}px`
7187
+ );
7188
+ host.style.setProperty("--ns-assistant-default-z-index", String(appearance.layout.zIndex));
7189
+ host.style.setProperty("--ns-assistant-default-edge-offset", `${appearance.layout.edgeOffset}px`);
7190
+ const logo = root?.querySelector(".brand-logo");
7191
+ const logoUrl = appearance.brand.logo[mode];
7192
+ if (logo) {
7193
+ logo.hidden = !logoUrl;
7194
+ if (logoUrl) logo.src = logoUrl;
7195
+ }
7196
+ if (root) syncAssistantPresentation(host, root, appearance, mode, input.sessionState);
7197
+ return warnings;
7198
+ }
7199
+
6918
7200
  // src/presentation-styles.ts
6919
7201
  var presentationStyles = `<style>
6920
7202
  :host([data-mode="floating"]) {
@@ -7120,7 +7402,18 @@ var NoodleAssistantElement = class extends HTMLElementBase {
7120
7402
  #pendingUserEcho;
7121
7403
  #sessionState = "idle";
7122
7404
  #startOpenApplied = false;
7405
+ #hostAppearance;
7406
+ #appearanceWarningKeys = /* @__PURE__ */ new Set();
7407
+ #appearanceStyleSnapshot = /* @__PURE__ */ new Map();
7123
7408
  sessionEndpoint = "";
7409
+ get appearance() {
7410
+ return this.#hostAppearance;
7411
+ }
7412
+ set appearance(value) {
7413
+ this.#hostAppearance = value;
7414
+ this.#appearanceWarningKeys.clear();
7415
+ if (this.isConnected) this.#applyTheme();
7416
+ }
7124
7417
  #events = new AssistantElementEventController({
7125
7418
  element: this,
7126
7419
  messages: () => this.#messages,
@@ -7136,10 +7429,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
7136
7429
  this.#syncAppearance();
7137
7430
  this.#applyStartOpenPolicy();
7138
7431
  },
7139
- labels: () => ({
7140
- confirm: this.#appearance.labels.confirm,
7141
- cancel: this.#appearance.labels.cancel
7142
- }),
7432
+ labels: () => this.#appearance.labels,
7143
7433
  respond: (id, response) => this.respond(id, response),
7144
7434
  dispatchError: (detail) => this.#dispatchError(detail),
7145
7435
  renderView: (detail) => {
@@ -7200,6 +7490,15 @@ var NoodleAssistantElement = class extends HTMLElementBase {
7200
7490
  focusComposer() {
7201
7491
  this.shadowRoot?.querySelector("textarea")?.focus();
7202
7492
  }
7493
+ /** Stop the active stream without discarding the current composer draft or session. */
7494
+ stop() {
7495
+ if (!this.hasAttribute("busy")) return;
7496
+ this.#conversationGeneration += 1;
7497
+ this.#client?.abort();
7498
+ this.#setBusy(false);
7499
+ this.#appendStatus(this.#appearance.labels.stopped);
7500
+ this.dispatchEvent(new CustomEvent("assistant-stopped"));
7501
+ }
7203
7502
  resetSession() {
7204
7503
  this.#conversationGeneration += 1;
7205
7504
  this.#client?.abort();
@@ -7214,6 +7513,20 @@ var NoodleAssistantElement = class extends HTMLElementBase {
7214
7513
  this.#setBusy(false);
7215
7514
  this.#setSessionState("idle");
7216
7515
  }
7516
+ /** Re-establish the session without replaying a message or interaction decision. */
7517
+ reconnect() {
7518
+ this.#conversationGeneration += 1;
7519
+ this.#client?.abort();
7520
+ this.#unsubscribeClient?.();
7521
+ this.#client = void 0;
7522
+ this.#clientEndpoint = "";
7523
+ this.#unsubscribeClient = void 0;
7524
+ this.#sessionBootstrap = void 0;
7525
+ this.#setBusy(false);
7526
+ this.#setSessionState("loading");
7527
+ this.#primeSession();
7528
+ this.dispatchEvent(new CustomEvent("assistant-reconnecting"));
7529
+ }
7217
7530
  updateContext(context) {
7218
7531
  this.#context = { ...context };
7219
7532
  if (this.#client) {
@@ -7261,6 +7574,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
7261
7574
  await this.#ensureClient().sendMessage(message);
7262
7575
  } catch (error) {
7263
7576
  if (generation !== this.#conversationGeneration) return;
7577
+ if (error instanceof AssistantClientError && error.detail.code === "request_aborted") return;
7264
7578
  this.#dispatchClientError(error, "turn_failed");
7265
7579
  throw error;
7266
7580
  } finally {
@@ -7340,26 +7654,31 @@ var NoodleAssistantElement = class extends HTMLElementBase {
7340
7654
  #handleClientEvent(event) {
7341
7655
  this.dispatchEvent(new CustomEvent("assistant-event", { detail: event }));
7342
7656
  this.#events.handle(event);
7343
- if (event.event === "session_started") this.#setSessionState("ready");
7657
+ if (event.event === "session_started") {
7658
+ this.#setSessionState("ready");
7659
+ this.#messages?.querySelector(".conversation-error")?.remove();
7660
+ }
7344
7661
  if (event.event === "session_expired" || event.event === "session_reset") {
7345
7662
  this.#setSessionState("idle");
7346
7663
  }
7347
7664
  if (event.event === "tool_proposed" || event.event === "interaction_proposed" || event.event === "input_requested") {
7348
7665
  this.#removeThinking();
7349
7666
  this.setAttribute("has-messages", "");
7350
- this.#scrollToBottom();
7351
7667
  }
7352
7668
  if (event.event === "content" || event.event === "message_completed" || event.event === "interaction_completed" || event.event === "error") {
7353
7669
  this.#removeThinking();
7354
- this.#scrollToBottom();
7355
7670
  }
7356
7671
  }
7357
7672
  #setBusy(busy) {
7358
7673
  this.toggleAttribute("busy", busy);
7359
7674
  const textarea = this.shadowRoot?.querySelector("textarea");
7360
7675
  const send = this.shadowRoot?.querySelector(".send");
7361
- if (textarea) textarea.disabled = busy;
7362
- if (send) send.disabled = busy;
7676
+ if (textarea) textarea.disabled = false;
7677
+ if (send) {
7678
+ send.dataset.action = busy ? "stop" : "send";
7679
+ send.textContent = busy ? this.#appearance.labels.stop : this.#appearance.labels.send;
7680
+ send.setAttribute("aria-label", send.textContent);
7681
+ }
7363
7682
  if (!busy) {
7364
7683
  this.#removeThinking();
7365
7684
  return;
@@ -7397,6 +7716,16 @@ var NoodleAssistantElement = class extends HTMLElementBase {
7397
7716
  body.textContent = text2;
7398
7717
  }
7399
7718
  message.append(body);
7719
+ if (role === "assistant" && text2) {
7720
+ const copy = document.createElement("button");
7721
+ copy.type = "button";
7722
+ copy.className = "message-copy";
7723
+ copy.textContent = this.#appearance.labels.copy;
7724
+ copy.addEventListener("click", () => {
7725
+ void navigator.clipboard?.writeText(text2);
7726
+ });
7727
+ message.append(copy);
7728
+ }
7400
7729
  if (this.#appearance.behavior.showTimestamps) {
7401
7730
  const timestamp = document.createElement("time");
7402
7731
  timestamp.dateTime = (/* @__PURE__ */ new Date()).toISOString();
@@ -7408,12 +7737,31 @@ var NoodleAssistantElement = class extends HTMLElementBase {
7408
7737
  }
7409
7738
  this.#messages.append(message);
7410
7739
  this.setAttribute("has-messages", "");
7411
- this.#scrollToBottom();
7740
+ this.#revealLatest();
7412
7741
  return body;
7413
7742
  }
7743
+ #appendStatus(text2) {
7744
+ if (!this.#messages) return;
7745
+ const status = document.createElement("p");
7746
+ status.className = "conversation-status";
7747
+ status.setAttribute("role", "status");
7748
+ status.textContent = text2;
7749
+ this.#messages.append(status);
7750
+ }
7414
7751
  #scrollToBottom() {
7415
7752
  if (this.#messages) this.#messages.scrollTop = this.#messages.scrollHeight;
7416
7753
  }
7754
+ #revealLatest() {
7755
+ if (!this.#messages) return;
7756
+ const distance = this.#messages.scrollHeight - this.#messages.scrollTop - this.#messages.clientHeight;
7757
+ const control = this.shadowRoot?.querySelector(".new-messages");
7758
+ if (distance <= 160) {
7759
+ this.#scrollToBottom();
7760
+ if (control) control.hidden = true;
7761
+ } else if (control) {
7762
+ control.hidden = false;
7763
+ }
7764
+ }
7417
7765
  #dispatchError(detail) {
7418
7766
  this.dispatchEvent(new CustomEvent("assistant-error", { detail }));
7419
7767
  }
@@ -7421,9 +7769,27 @@ var NoodleAssistantElement = class extends HTMLElementBase {
7421
7769
  if (error instanceof AssistantClientError && error.detail.code === "session_failed") {
7422
7770
  this.#setSessionState("error");
7423
7771
  }
7424
- this.#dispatchError(
7425
- error instanceof AssistantClientError ? error.detail : { code: fallbackCode, retryable: false }
7772
+ const detail = error instanceof AssistantClientError ? error.detail : { code: fallbackCode, retryable: false };
7773
+ this.#appendRecoveryError(
7774
+ detail.code === "session_expired" ? this.#appearance.labels.sessionExpired : this.#appearance.labels.unavailable
7426
7775
  );
7776
+ this.#dispatchError(detail);
7777
+ }
7778
+ #appendRecoveryError(message) {
7779
+ if (!this.#messages) return;
7780
+ this.#messages.querySelector(".conversation-error")?.remove();
7781
+ const error = document.createElement("section");
7782
+ error.className = "conversation-error";
7783
+ error.setAttribute("role", "alert");
7784
+ const text2 = document.createElement("p");
7785
+ text2.textContent = message;
7786
+ const retry = document.createElement("button");
7787
+ retry.type = "button";
7788
+ retry.textContent = this.#appearance.labels.reconnect;
7789
+ retry.addEventListener("click", () => this.reconnect());
7790
+ error.append(text2, retry);
7791
+ this.#messages.append(error);
7792
+ this.#revealLatest();
7427
7793
  }
7428
7794
  #resolvedMode() {
7429
7795
  const configured = this.getAttribute("theme") ?? this.#appearance.theme.defaultMode;
@@ -7431,43 +7797,24 @@ var NoodleAssistantElement = class extends HTMLElementBase {
7431
7797
  }
7432
7798
  #applyTheme() {
7433
7799
  const mode = this.#resolvedMode();
7434
- this.setAttribute("data-theme", mode);
7435
- const tokens = this.#appearance.theme[mode];
7436
- for (const [name, value] of Object.entries(tokens)) {
7437
- this.style.setProperty(
7438
- `--ns-assistant-default-${camelToKebab(name)}`,
7439
- tokenValue(name, value)
7800
+ for (const warning of applyAssistantElementTheme({
7801
+ host: this,
7802
+ root: this.shadowRoot,
7803
+ appearance: this.#appearance,
7804
+ hostAppearance: this.#hostAppearance,
7805
+ mode,
7806
+ sessionState: this.#sessionState,
7807
+ styleSnapshot: this.#appearanceStyleSnapshot
7808
+ })) {
7809
+ const key = `${warning.theme}:${warning.role}:${warning.foreground}:${warning.background}`;
7810
+ if (this.#appearanceWarningKeys.has(key)) continue;
7811
+ this.#appearanceWarningKeys.add(key);
7812
+ this.dispatchEvent(
7813
+ new CustomEvent("assistant-appearance-warning", {
7814
+ detail: warning
7815
+ })
7440
7816
  );
7441
7817
  }
7442
- this.style.setProperty(
7443
- "--ns-assistant-default-panel-width",
7444
- `${this.#appearance.layout.panelWidth}px`
7445
- );
7446
- this.style.setProperty(
7447
- "--ns-assistant-default-min-height",
7448
- `${this.#appearance.layout.panelMinHeight}px`
7449
- );
7450
- this.style.setProperty(
7451
- "--ns-assistant-default-max-height",
7452
- `${this.#appearance.layout.panelMaxHeight}px`
7453
- );
7454
- this.style.setProperty(
7455
- "--ns-assistant-default-z-index",
7456
- String(this.#appearance.layout.zIndex)
7457
- );
7458
- this.style.setProperty(
7459
- "--ns-assistant-default-edge-offset",
7460
- `${this.#appearance.layout.edgeOffset}px`
7461
- );
7462
- const logo = this.shadowRoot?.querySelector(".brand-logo");
7463
- const logoUrl = this.#appearance.brand.logo[mode];
7464
- if (logo) {
7465
- logo.hidden = !logoUrl;
7466
- if (logoUrl) logo.src = logoUrl;
7467
- }
7468
- if (this.shadowRoot) {
7469
- syncAssistantPresentation(this, this.shadowRoot, this.#appearance, mode, this.#sessionState);
7470
- }
7471
7818
  }
7472
7819
  #setSessionState(state) {
7473
7820
  this.#sessionState = state;
@@ -7547,6 +7894,7 @@ var NoodleAssistantElement = class extends HTMLElementBase {
7547
7894
  <div class="welcome"><slot name="empty-state"></slot></div>
7548
7895
  <div class="suggested-prompts"></div>
7549
7896
  <div class="messages" aria-live="polite"></div>
7897
+ <button class="new-messages" type="button" hidden></button>
7550
7898
  <form><slot name="composer-leading"></slot><textarea rows="1"></textarea><button class="send" type="submit"></button><slot name="composer-trailing"></slot></form>
7551
7899
  <nav class="legal" aria-label="Legal"></nav>
7552
7900
  <slot name="conversation-footer"></slot>
@@ -7589,6 +7937,10 @@ var NoodleAssistantElement = class extends HTMLElementBase {
7589
7937
  const form = queryRequired(this.shadowRoot, "form");
7590
7938
  form.addEventListener("submit", (event) => {
7591
7939
  event.preventDefault();
7940
+ if (this.hasAttribute("busy")) {
7941
+ this.stop();
7942
+ return;
7943
+ }
7592
7944
  const value = textarea.value;
7593
7945
  textarea.value = "";
7594
7946
  textarea.style.height = "auto";
@@ -7603,6 +7955,17 @@ var NoodleAssistantElement = class extends HTMLElementBase {
7603
7955
  event.preventDefault();
7604
7956
  form.requestSubmit();
7605
7957
  });
7958
+ const newMessages = queryRequired(this.shadowRoot, ".new-messages");
7959
+ newMessages.textContent = appearance.labels.newMessages;
7960
+ newMessages.addEventListener("click", () => {
7961
+ this.#scrollToBottom();
7962
+ newMessages.hidden = true;
7963
+ });
7964
+ this.#messages?.addEventListener("scroll", () => {
7965
+ if (this.#messages && this.#messages.scrollHeight - this.#messages.scrollTop - this.#messages.clientHeight < 48) {
7966
+ newMessages.hidden = true;
7967
+ }
7968
+ });
7606
7969
  this.#applyTheme();
7607
7970
  }
7608
7971
  #appendLegalLinks(legal) {
@@ -7636,4 +7999,4 @@ export {
7636
7999
  dompurify/dist/purify.es.mjs:
7637
8000
  (*! @license DOMPurify 3.4.11 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.11/LICENSE *)
7638
8001
  */
7639
- //# sourceMappingURL=chunk-JIL4B5HD.js.map
8002
+ //# sourceMappingURL=chunk-ZKXOX67J.js.map