@artooi/ag-ui-web-component 0.16.0 → 0.18.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.js CHANGED
@@ -3,6 +3,7 @@ var ELEMENT_TAG = "ag-ui-chat";
3
3
  var SUBMIT_EVENT = "ag-ui-submit";
4
4
  var TOGGLE_EVENT = "ag-ui-toggle";
5
5
  var STATE_EVENT = "ag-ui-state";
6
+ var ATTACHMENT_EVENT = "ag-ui-attachments";
6
7
  var MESSAGE_ROLE = {
7
8
  USER: "user",
8
9
  ASSISTANT: "assistant"
@@ -449,6 +450,9 @@ var DEFAULT_UI_STRINGS = {
449
450
  collapse: "Collapse",
450
451
  expand: "Expand",
451
452
  toggleTheme: "Toggle theme",
453
+ copyCode: "Copy",
454
+ copied: "Copied",
455
+ copyFailed: "Copy failed",
452
456
  checkpoints: "Continue a run",
453
457
  noCheckpoints: "Nothing to continue yet.",
454
458
  resumeRun: "Resume",
@@ -466,6 +470,7 @@ var DEFAULT_UI_STRINGS = {
466
470
  usingSkill: "Using skill {name}",
467
471
  runInterrupted: "The previous response didn\u2019t finish \u2014 the page changed before it arrived.",
468
472
  pageMoved: "The page changed since you last looked at it. Call read_page to see the current page, then retry.",
473
+ attachmentsStillUploading: "{n} file still uploading \u2014 it was not sent with this message and is still attached.",
469
474
  skillNeeds: "\u201C{title}\u201D needs: {fields}",
470
475
  message: "Message",
471
476
  inputPlaceholder: "Ask anything\u2026",
@@ -529,12 +534,12 @@ function mergeUiStrings(overrides) {
529
534
 
530
535
  // src/ui/approval_card.ts
531
536
  function actionButton(modifier, label) {
532
- const button = document.createElement("button");
533
- button.type = "button";
534
- button.className = `approval-btn approval-btn--${modifier}`;
535
- button.setAttribute("part", `approval-button approval-${modifier}`);
536
- button.textContent = label;
537
- return button;
537
+ const button2 = document.createElement("button");
538
+ button2.type = "button";
539
+ button2.className = `approval-btn approval-btn--${modifier}`;
540
+ button2.setAttribute("part", `approval-button approval-${modifier}`);
541
+ button2.textContent = label;
542
+ return button2;
538
543
  }
539
544
  function requestApproval(host, request, options = {}) {
540
545
  const strings = options.strings ?? DEFAULT_UI_STRINGS;
@@ -581,6 +586,55 @@ function requestApproval(host, request, options = {}) {
581
586
  });
582
587
  }
583
588
 
589
+ // src/ui/attach_copy_buttons.ts
590
+ var CONFIRM_MS = 1500;
591
+ function attachCopyButtons(root, strings) {
592
+ for (const pre of Array.from(root.querySelectorAll("pre"))) {
593
+ const code = pre.querySelector("code");
594
+ if (code === null || pre.querySelector(".code-copy") !== null) {
595
+ continue;
596
+ }
597
+ pre.classList.add("has-copy");
598
+ pre.append(button(code, strings));
599
+ }
600
+ }
601
+ function button(code, strings) {
602
+ const text2 = code.textContent;
603
+ const copy = document.createElement("button");
604
+ copy.type = "button";
605
+ copy.className = "code-copy";
606
+ copy.setAttribute("part", "code-copy");
607
+ copy.textContent = strings.copyCode;
608
+ copy.title = strings.copyCode;
609
+ copy.setAttribute("aria-label", strings.copyCode);
610
+ copy.addEventListener("click", () => {
611
+ void writeText(text2).then((ok) => {
612
+ confirm(copy, ok ? strings.copied : strings.copyFailed, strings);
613
+ });
614
+ });
615
+ return copy;
616
+ }
617
+ async function writeText(text2) {
618
+ const clipboard = navigator.clipboard;
619
+ if (clipboard === void 0) {
620
+ return false;
621
+ }
622
+ try {
623
+ await clipboard.writeText(text2);
624
+ return true;
625
+ } catch {
626
+ return false;
627
+ }
628
+ }
629
+ function confirm(copy, message, strings) {
630
+ copy.textContent = message;
631
+ copy.dataset["state"] = message === strings.copied ? "copied" : "failed";
632
+ setTimeout(() => {
633
+ copy.textContent = strings.copyCode;
634
+ delete copy.dataset["state"];
635
+ }, CONFIRM_MS);
636
+ }
637
+
584
638
  // src/ui/attachment_chips.ts
585
639
  function renderAttachmentChips(refs) {
586
640
  const list = document.createElement("div");
@@ -693,6 +747,10 @@ var AttachmentTray = class {
693
747
  hasPending() {
694
748
  return this.#items.some((item) => item.status === ATTACHMENT_STATUS.UPLOADING);
695
749
  }
750
+ /** How many chips are still uploading — the count a send-time notice names. */
751
+ pendingCount() {
752
+ return this.#items.filter((item) => item.status === ATTACHMENT_STATUS.UPLOADING).length;
753
+ }
696
754
  /** Whether the tray holds no chips. */
697
755
  isEmpty() {
698
756
  return this.#items.length === 0;
@@ -879,6 +937,8 @@ var CheckpointMenu = class {
879
937
  #onPick;
880
938
  #list;
881
939
  #heading;
940
+ /** What had focus before the panel opened, restored on close. */
941
+ #lastFocused = null;
882
942
  #strings;
883
943
  #runs = [];
884
944
  constructor(onPick, strings = DEFAULT_UI_STRINGS) {
@@ -889,24 +949,21 @@ var CheckpointMenu = class {
889
949
  this.element.setAttribute("part", "checkpoints");
890
950
  this.element.setAttribute("role", "dialog");
891
951
  this.element.setAttribute("aria-label", strings.checkpoints);
952
+ this.element.tabIndex = -1;
892
953
  this.element.hidden = true;
893
954
  const header = document.createElement("div");
894
955
  header.className = "checkpoints-header";
895
956
  header.setAttribute("part", "checkpoints-header");
896
957
  this.#heading = document.createElement("span");
897
958
  this.#heading.className = "checkpoints-title";
959
+ this.#heading.setAttribute("part", "checkpoints-title");
898
960
  this.#heading.textContent = strings.checkpoints;
899
961
  header.append(this.#heading);
900
962
  this.#list = document.createElement("div");
901
963
  this.#list.className = "checkpoints-list";
902
964
  this.#list.setAttribute("part", "checkpoints-list");
903
965
  this.element.append(header, this.#list);
904
- this.element.addEventListener("keydown", (event) => {
905
- if (event.key === "Escape") {
906
- event.stopPropagation();
907
- this.close();
908
- }
909
- });
966
+ this.element.addEventListener("keydown", (event) => this.#onKeydown(event));
910
967
  }
911
968
  /** Replace the rows. The host passes only `continuable` runs. */
912
969
  setRuns(runs) {
@@ -921,10 +978,52 @@ var CheckpointMenu = class {
921
978
  this.#render();
922
979
  }
923
980
  open() {
981
+ if (this.open_) {
982
+ return;
983
+ }
984
+ this.#lastFocused = this.#activeElement();
924
985
  this.element.hidden = false;
986
+ (this.#focusables()[0] ?? this.element).focus();
925
987
  }
926
988
  close() {
989
+ if (!this.open_) {
990
+ return;
991
+ }
927
992
  this.element.hidden = true;
993
+ this.#lastFocused?.focus();
994
+ this.#lastFocused = null;
995
+ }
996
+ /** The focused element within this panel's root (shadow-aware). */
997
+ #activeElement() {
998
+ return this.element.getRootNode().activeElement;
999
+ }
1000
+ /** The panel's tabbable controls, in document order. */
1001
+ #focusables() {
1002
+ return Array.from(this.element.querySelectorAll("button, [tabindex]")).filter(
1003
+ (el) => !el.hidden
1004
+ );
1005
+ }
1006
+ /** Escape closes; Tab is trapped so focus cannot wander behind the dialog. */
1007
+ #onKeydown(event) {
1008
+ if (event.key === "Escape") {
1009
+ event.stopPropagation();
1010
+ this.close();
1011
+ return;
1012
+ }
1013
+ if (event.key !== "Tab") {
1014
+ return;
1015
+ }
1016
+ const focusables = this.#focusables();
1017
+ const first = focusables[0];
1018
+ const last = focusables[focusables.length - 1];
1019
+ const active = this.#activeElement();
1020
+ if (event.shiftKey && active === first) {
1021
+ event.preventDefault();
1022
+ last?.focus();
1023
+ } else if (!event.shiftKey && active === last) {
1024
+ event.preventDefault();
1025
+ first?.focus();
1026
+ }
928
1027
  }
929
1028
  get open_() {
930
1029
  return !this.element.hidden;
@@ -949,6 +1048,7 @@ var CheckpointMenu = class {
949
1048
  row.setAttribute("part", "checkpoint-row");
950
1049
  const label = document.createElement("span");
951
1050
  label.className = "checkpoint-label";
1051
+ label.setAttribute("part", "checkpoint-label");
952
1052
  label.textContent = run.started_at === null ? run.run_id : relativeTime(Date.parse(run.started_at), Date.now(), this.#strings);
953
1053
  label.title = run.run_id;
954
1054
  row.append(label);
@@ -967,27 +1067,27 @@ var CheckpointMenu = class {
967
1067
  return row;
968
1068
  }
969
1069
  #action(runId, verb, label) {
970
- const button = document.createElement("button");
971
- button.type = "button";
972
- button.className = `checkpoint-action checkpoint-${verb}`;
973
- button.setAttribute("part", `checkpoint-action checkpoint-${verb}`);
974
- button.textContent = label;
975
- button.addEventListener("click", () => {
1070
+ const button2 = document.createElement("button");
1071
+ button2.type = "button";
1072
+ button2.className = `checkpoint-action checkpoint-${verb}`;
1073
+ button2.setAttribute("part", `checkpoint-action checkpoint-${verb}`);
1074
+ button2.textContent = label;
1075
+ button2.addEventListener("click", () => {
976
1076
  this.close();
977
1077
  this.#onPick(runId, verb);
978
1078
  });
979
- return button;
1079
+ return button2;
980
1080
  }
981
1081
  };
982
1082
 
983
1083
  // src/ui/confirmation_card.ts
984
1084
  function actionButton2(modifier, label) {
985
- const button = document.createElement("button");
986
- button.type = "button";
987
- button.className = `confirm-btn confirm-btn--${modifier}`;
988
- button.setAttribute("part", `confirm-button confirm-${modifier}`);
989
- button.textContent = label;
990
- return button;
1085
+ const button2 = document.createElement("button");
1086
+ button2.type = "button";
1087
+ button2.className = `confirm-btn confirm-btn--${modifier}`;
1088
+ button2.setAttribute("part", `confirm-button confirm-${modifier}`);
1089
+ button2.textContent = label;
1090
+ return button2;
991
1091
  }
992
1092
  function requestConfirmation(host, request, options = {}) {
993
1093
  const strings = options.strings ?? DEFAULT_UI_STRINGS;
@@ -1010,7 +1110,7 @@ function requestConfirmation(host, request, options = {}) {
1010
1110
  actions.className = "confirm-actions";
1011
1111
  actions.setAttribute("part", "confirm-actions");
1012
1112
  const cancel = actionButton2("cancel", strings.cancel);
1013
- const confirm = actionButton2("confirm", strings.confirm);
1113
+ const confirm2 = actionButton2("confirm", strings.confirm);
1014
1114
  let settled = false;
1015
1115
  const close = (accepted) => {
1016
1116
  if (settled) {
@@ -1018,21 +1118,21 @@ function requestConfirmation(host, request, options = {}) {
1018
1118
  }
1019
1119
  settled = true;
1020
1120
  cancel.disabled = true;
1021
- confirm.disabled = true;
1121
+ confirm2.disabled = true;
1022
1122
  card.setAttribute("data-resolved", accepted ? "confirmed" : "declined");
1023
1123
  resolve(accepted);
1024
1124
  };
1025
1125
  cancel.addEventListener("click", () => close(false));
1026
- confirm.addEventListener("click", () => close(true));
1126
+ confirm2.addEventListener("click", () => close(true));
1027
1127
  options.signal?.addEventListener("abort", () => close(false), { once: true });
1028
- actions.append(cancel, confirm);
1128
+ actions.append(cancel, confirm2);
1029
1129
  card.append(body, args, actions);
1030
1130
  host.appendChild(card);
1031
1131
  if (options.signal?.aborted === true) {
1032
1132
  close(false);
1033
1133
  return;
1034
1134
  }
1035
- confirm.focus();
1135
+ confirm2.focus();
1036
1136
  });
1037
1137
  }
1038
1138
 
@@ -4185,13 +4285,13 @@ var SkillsMenu = class {
4185
4285
  const chipSkills = this.#chipsEnabled ? this.#skills.filter((skill) => skill.chip === true) : [];
4186
4286
  this.chips.hidden = chipSkills.length === 0;
4187
4287
  for (const skill of chipSkills) {
4188
- const button = document.createElement("button");
4189
- button.type = "button";
4190
- button.className = "skill-chip";
4191
- button.setAttribute("part", "skill-chip");
4192
- button.textContent = skill.title;
4193
- button.addEventListener("click", () => this.#pick(skill));
4194
- this.chips.appendChild(button);
4288
+ const button2 = document.createElement("button");
4289
+ button2.type = "button";
4290
+ button2.className = "skill-chip";
4291
+ button2.setAttribute("part", "skill-chip");
4292
+ button2.textContent = skill.title;
4293
+ button2.addEventListener("click", () => this.#pick(skill));
4294
+ this.chips.appendChild(button2);
4195
4295
  }
4196
4296
  }
4197
4297
  #renderPalette() {
@@ -4231,6 +4331,7 @@ var STYLES = `
4231
4331
  --ag-ui-user-bg: #4f46e5;
4232
4332
  --ag-ui-user-fg: #ffffff;
4233
4333
  --ag-ui-assistant-bg: #f1f1f6;
4334
+ --ag-ui-hover: #e7e7ee;
4234
4335
  --ag-ui-input-bg: var(--ag-ui-bg);
4235
4336
  --ag-ui-tool-bg: var(--ag-ui-assistant-bg);
4236
4337
  --ag-ui-tool-fg: var(--ag-ui-accent);
@@ -4300,6 +4401,7 @@ var STYLES = `
4300
4401
  --ag-ui-bg: #15151f;
4301
4402
  --ag-ui-fg: #e8e8f2;
4302
4403
  --ag-ui-assistant-bg: #25253a;
4404
+ --ag-ui-hover: #303049;
4303
4405
  --ag-ui-header-bg: #1f1f30;
4304
4406
  --ag-ui-header-fg: #e8e8f2;
4305
4407
  --ag-ui-border: #33334a;
@@ -4312,6 +4414,7 @@ var STYLES = `
4312
4414
  --ag-ui-bg: #15151f;
4313
4415
  --ag-ui-fg: #e8e8f2;
4314
4416
  --ag-ui-assistant-bg: #25253a;
4417
+ --ag-ui-hover: #303049;
4315
4418
  --ag-ui-header-bg: #1f1f30;
4316
4419
  --ag-ui-header-fg: #e8e8f2;
4317
4420
  --ag-ui-border: #33334a;
@@ -4327,6 +4430,7 @@ var STYLES = `
4327
4430
  --ag-ui-accent: #3fb950;
4328
4431
  --ag-ui-user-bg: #238636;
4329
4432
  --ag-ui-assistant-bg: #161b22;
4433
+ --ag-ui-hover: #21262d;
4330
4434
  --ag-ui-header-bg: #010409;
4331
4435
  --ag-ui-header-fg: #c9d1d9;
4332
4436
  --ag-ui-border: #30363d;
@@ -4728,12 +4832,86 @@ var STYLES = `
4728
4832
  padding: 0;
4729
4833
  }
4730
4834
 
4835
+ /* The copy button sits inside the block, so it scrolls with wide code rather
4836
+ than floating over the bubble. Positioning is on the pre; the button only
4837
+ appears once one has been attached. */
4838
+ .message--assistant pre.has-copy {
4839
+ position: relative;
4840
+ }
4841
+
4842
+ .code-copy {
4843
+ position: absolute;
4844
+ top: 4px;
4845
+ right: 4px;
4846
+ padding: 2px 8px;
4847
+ font: inherit;
4848
+ font-size: 0.75em;
4849
+ line-height: 1.6;
4850
+ color: var(--ag-ui-muted);
4851
+ background: var(--ag-ui-surface);
4852
+ border: 1px solid var(--ag-ui-border);
4853
+ border-radius: 4px;
4854
+ cursor: pointer;
4855
+ opacity: 0;
4856
+ transition: opacity 0.12s ease;
4857
+ }
4858
+
4859
+ /* Revealed on hover or keyboard focus. Focus matters as much as hover here:
4860
+ hidden-until-hover is invisible to a keyboard user otherwise. */
4861
+ .message--assistant pre.has-copy:hover .code-copy,
4862
+ .code-copy:focus-visible {
4863
+ opacity: 1;
4864
+ }
4865
+
4866
+ .code-copy:hover {
4867
+ color: var(--ag-ui-text);
4868
+ background: var(--ag-ui-hover);
4869
+ }
4870
+
4871
+ .code-copy[data-state="copied"] {
4872
+ opacity: 1;
4873
+ color: var(--ag-ui-text);
4874
+ }
4875
+
4876
+ .code-copy[data-state="failed"] {
4877
+ opacity: 1;
4878
+ color: var(--ag-ui-danger, var(--ag-ui-text));
4879
+ }
4880
+
4731
4881
  .message--assistant blockquote {
4732
4882
  padding-left: 10px;
4733
4883
  border-left: 3px solid var(--ag-ui-border);
4734
4884
  color: var(--ag-ui-muted);
4735
4885
  }
4736
4886
 
4887
+ /* Markdown tables. table/thead/tbody/tr/th/td are all in
4888
+ the sanitizer's ALLOWED_TAGS, so an agent emitting one renders it \u2014 and until
4889
+ now rendered it entirely unstyled, overflowing the bubble. A wide table
4890
+ scrolls inside its own box rather than stretching the message: the bubble is
4891
+ width-constrained, so without this the columns either crush or push the
4892
+ layout sideways. */
4893
+ .message--assistant table {
4894
+ display: block;
4895
+ width: fit-content;
4896
+ max-width: 100%;
4897
+ overflow-x: auto;
4898
+ border-collapse: collapse;
4899
+ font-size: 0.95em;
4900
+ }
4901
+
4902
+ .message--assistant th,
4903
+ .message--assistant td {
4904
+ padding: 6px 10px;
4905
+ border: 1px solid var(--ag-ui-border);
4906
+ text-align: left;
4907
+ vertical-align: top;
4908
+ }
4909
+
4910
+ .message--assistant th {
4911
+ background: var(--ag-ui-hover);
4912
+ font-weight: 600;
4913
+ }
4914
+
4737
4915
  .pending {
4738
4916
  align-self: flex-start;
4739
4917
  display: inline-flex;
@@ -5484,9 +5662,9 @@ var STYLES = `
5484
5662
  flex-direction: column;
5485
5663
  gap: 0.25rem;
5486
5664
  padding: 0.5rem;
5487
- border: 1px solid var(--agui-border, #d7d7dc);
5665
+ border: 1px solid var(--ag-ui-border);
5488
5666
  border-radius: 0.5rem;
5489
- background: var(--agui-surface, #fff);
5667
+ background: var(--ag-ui-assistant-bg);
5490
5668
  box-shadow: 0 6px 24px rgb(0 0 0 / 12%);
5491
5669
  max-height: 60%;
5492
5670
  overflow-y: auto;
@@ -5517,7 +5695,7 @@ var STYLES = `
5517
5695
  }
5518
5696
 
5519
5697
  .checkpoint-row:hover {
5520
- background: var(--agui-hover, #f3f3f5);
5698
+ background: var(--ag-ui-hover);
5521
5699
  }
5522
5700
 
5523
5701
  .checkpoint-label {
@@ -5532,7 +5710,7 @@ var STYLES = `
5532
5710
  font-size: 0.6875rem;
5533
5711
  padding: 0 0.375rem;
5534
5712
  border-radius: 999px;
5535
- background: var(--agui-hover, #f3f3f5);
5713
+ background: var(--ag-ui-hover);
5536
5714
  opacity: 0.8;
5537
5715
  }
5538
5716
 
@@ -5541,14 +5719,14 @@ var STYLES = `
5541
5719
  font-size: 0.75rem;
5542
5720
  cursor: pointer;
5543
5721
  padding: 0.125rem 0.5rem;
5544
- border: 1px solid var(--agui-border, #d7d7dc);
5722
+ border: 1px solid var(--ag-ui-border);
5545
5723
  border-radius: 0.375rem;
5546
5724
  background: transparent;
5547
5725
  color: inherit;
5548
5726
  }
5549
5727
 
5550
5728
  .checkpoint-action:hover {
5551
- background: var(--agui-hover, #f3f3f5);
5729
+ background: var(--ag-ui-hover);
5552
5730
  }
5553
5731
 
5554
5732
  .drawer-backdrop {
@@ -6013,9 +6191,9 @@ var ThreadDrawer = class {
6013
6191
  }
6014
6192
  /** Swap a row for an inline "Delete? [Delete] [Cancel]" confirm. */
6015
6193
  #confirmDelete(row, meta) {
6016
- const confirm = document.createElement("div");
6017
- confirm.className = "drawer-confirm";
6018
- confirm.setAttribute("part", "drawer-confirm");
6194
+ const confirm2 = document.createElement("div");
6195
+ confirm2.className = "drawer-confirm";
6196
+ confirm2.setAttribute("part", "drawer-confirm");
6019
6197
  const label = document.createElement("span");
6020
6198
  label.className = "drawer-confirm-label";
6021
6199
  label.setAttribute("part", "drawer-confirm-label");
@@ -6032,8 +6210,8 @@ var ThreadDrawer = class {
6032
6210
  no.setAttribute("part", "drawer-confirm-no");
6033
6211
  no.textContent = this.#strings.cancel;
6034
6212
  no.addEventListener("click", () => this.#renderList());
6035
- confirm.append(label, yes, no);
6036
- row.replaceChildren(confirm);
6213
+ confirm2.append(label, yes, no);
6214
+ row.replaceChildren(confirm2);
6037
6215
  }
6038
6216
  };
6039
6217
 
@@ -6999,6 +7177,21 @@ function errorMessage2(xhr) {
6999
7177
  }
7000
7178
 
7001
7179
  // src/core/ag_ui_chat.ts
7180
+ var CONNECT_TIME_ATTRIBUTES = [
7181
+ "data-attachments-url",
7182
+ "data-attachment-accept",
7183
+ "data-attachment-max-bytes",
7184
+ "data-transcribe-url",
7185
+ "data-threads-url",
7186
+ "data-tools-url",
7187
+ "data-skills-url",
7188
+ "data-skills",
7189
+ "data-prompt-chips",
7190
+ "data-slash-commands",
7191
+ "data-theme-toggle",
7192
+ "data-strings",
7193
+ "data-icon-url"
7194
+ ];
7002
7195
  var COLLAPSED_KEY = "ag-ui-chat:collapsed";
7003
7196
  var THEME_KEY = "ag-ui-chat:theme";
7004
7197
  var AgUiChat = class extends HTMLElement {
@@ -7198,6 +7391,8 @@ var AgUiChat = class extends HTMLElement {
7198
7391
  #voiceSlot;
7199
7392
  /** Voice-input control; created on connect when transcription is available. */
7200
7393
  #voice = null;
7394
+ /** Whether the element is currently in the DOM; gates the connect-time warning. */
7395
+ #connected = false;
7201
7396
  /** Refs attached to the message currently being sent (the context manifest). */
7202
7397
  #runAttachments = [];
7203
7398
  #client = null;
@@ -7344,12 +7539,21 @@ var AgUiChat = class extends HTMLElement {
7344
7539
  const index = this.#runs();
7345
7540
  this.#checkpoints.setRuns(index === null ? [] : await index.continuable());
7346
7541
  }
7347
- /** Attributes whose late changes must reflect in already-rendered chrome. */
7542
+ /** Attributes the element reacts to after it has been connected. */
7348
7543
  static get observedAttributes() {
7349
- return ["title-text"];
7544
+ return ["title-text", ...CONNECT_TIME_ATTRIBUTES];
7350
7545
  }
7351
- attributeChangedCallback(_name, _previous, value) {
7352
- this.#title.textContent = value ?? this.#strings.title;
7546
+ attributeChangedCallback(name, previous, value) {
7547
+ if (name === "title-text") {
7548
+ this.#title.textContent = value ?? this.#strings.title;
7549
+ return;
7550
+ }
7551
+ if (previous === value || !this.#connected) {
7552
+ return;
7553
+ }
7554
+ console.warn(
7555
+ `<ag-ui-chat>: "${name}" was changed after the element connected, and is read only while connecting \u2014 this assignment has no effect. Set it before the element enters the DOM (in the markup, or on the element before appending it); frameworks that patch attributes after mount should bind it at creation. To apply a new value now, remove and re-insert the element.`
7556
+ );
7353
7557
  }
7354
7558
  /** Declare a frontend tool the agent may call. */
7355
7559
  registerTool(tool) {
@@ -7565,6 +7769,7 @@ var AgUiChat = class extends HTMLElement {
7565
7769
  this.#wireVoice();
7566
7770
  this.#threadId = this.conversationStore.threadId();
7567
7771
  void this.#rehydrate();
7772
+ this.#connected = true;
7568
7773
  }
7569
7774
  /**
7570
7775
  * Tear down live resources when the element leaves the DOM (a removed node, a
@@ -7575,10 +7780,27 @@ var AgUiChat = class extends HTMLElement {
7575
7780
  * `MediaRecorder`.
7576
7781
  */
7577
7782
  disconnectedCallback() {
7783
+ this.#connected = false;
7578
7784
  this.#cancelRun();
7579
7785
  this.#attachTray?.dispose();
7580
7786
  this.#voice?.dispose();
7581
7787
  }
7788
+ /**
7789
+ * Read an opt-in flag attribute the way HTML reads a boolean attribute.
7790
+ *
7791
+ * Present means on — bare (`data-prompt-chips`), empty
7792
+ * (`data-prompt-chips=""`) or any value except the literal `"false"`. These
7793
+ * were compared against the string `"true"`, so writing the attribute bare —
7794
+ * the spelling every native boolean attribute uses, and the one a reader
7795
+ * reaches for first — silently *disabled* the feature it names, with nothing
7796
+ * to indicate why the chips never appeared.
7797
+ *
7798
+ * `="false"` still turns it off, so an explicit opt-out keeps working.
7799
+ */
7800
+ #flag(name) {
7801
+ const value = this.getAttribute(name);
7802
+ return value !== null && value !== "false";
7803
+ }
7582
7804
  /** Parse the inline `data-strings` JSON overrides (empty when absent/malformed). */
7583
7805
  #readStringOverrides() {
7584
7806
  const raw = this.getAttribute("data-strings");
@@ -7608,12 +7830,18 @@ var AgUiChat = class extends HTMLElement {
7608
7830
  return;
7609
7831
  }
7610
7832
  const accept = this.getAttribute("data-attachment-accept") ?? "";
7611
- this.#attachTray = new AttachmentTray({
7833
+ const tray = new AttachmentTray({
7612
7834
  upload,
7613
7835
  maxBytes: this.#attachmentMaxBytes(),
7614
7836
  accept,
7615
- strings: this.#strings
7837
+ strings: this.#strings,
7838
+ // The tray's change hook, surfaced to the host as an event. A host
7839
+ // driving its own composer could otherwise not tell a settled upload from
7840
+ // one still in flight, which is the state sendMessage() has to be called
7841
+ // with knowledge of.
7842
+ onChange: () => this.#dispatchAttachments(tray)
7616
7843
  });
7844
+ this.#attachTray = tray;
7617
7845
  this.#attachSlot.appendChild(this.#attachTray.element);
7618
7846
  this.#fileInput.accept = accept;
7619
7847
  this.#attachButton.hidden = false;
@@ -7753,8 +7981,8 @@ Use the read_attachment tool with an id to read a file's contents.`
7753
7981
  }
7754
7982
  /** Wire the skill surfaces: opt-in flags, embedded catalog, optional fetch. */
7755
7983
  #initSkills() {
7756
- this.#skillsMenu.enableChips(this.getAttribute("data-prompt-chips") === "true");
7757
- this.#skillsMenu.enableSlash(this.getAttribute("data-slash-commands") === "true");
7984
+ this.#skillsMenu.enableChips(this.#flag("data-prompt-chips"));
7985
+ this.#skillsMenu.enableSlash(this.#flag("data-slash-commands"));
7758
7986
  this.#embedSkills = this.#readEmbeddedSkills();
7759
7987
  this.#recomputeSkills();
7760
7988
  void this.#fetchSkills();
@@ -8095,6 +8323,7 @@ Use the read_attachment tool with an id to read a file's contents.`
8095
8323
  bubble.setAttribute("part", `message message-${role}`);
8096
8324
  if (role === MESSAGE_ROLE.ASSISTANT) {
8097
8325
  bubble.innerHTML = renderMarkdown(content, { allowImages: this.allowImages });
8326
+ attachCopyButtons(bubble, this.#strings);
8098
8327
  this.#ensureGroup().appendChild(bubble);
8099
8328
  } else {
8100
8329
  this.#currentGroup = null;
@@ -8251,14 +8480,14 @@ Use the read_attachment tool with an id to read a file's contents.`
8251
8480
  }
8252
8481
  /** Build a header control button (icon glyph + localized title/aria). */
8253
8482
  #headerButton(modifier, label, glyph) {
8254
- const button = document.createElement("button");
8255
- button.type = "button";
8256
- button.className = `header-btn header-btn--${modifier}`;
8257
- button.setAttribute("part", `header-button ${modifier}-button`);
8258
- button.title = label;
8259
- button.setAttribute("aria-label", label);
8260
- button.textContent = glyph;
8261
- return button;
8483
+ const button2 = document.createElement("button");
8484
+ button2.type = "button";
8485
+ button2.className = `header-btn header-btn--${modifier}`;
8486
+ button2.setAttribute("part", `header-button ${modifier}-button`);
8487
+ button2.title = label;
8488
+ button2.setAttribute("aria-label", label);
8489
+ button2.textContent = glyph;
8490
+ return button2;
8262
8491
  }
8263
8492
  /**
8264
8493
  * An icon holder wrapping a `<slot>` so a host can project custom markup; with
@@ -8338,12 +8567,46 @@ Use the read_attachment tool with an id to read a file's contents.`
8338
8567
  if (content === "" && attachments.length === 0) {
8339
8568
  return;
8340
8569
  }
8570
+ this.#input.value = "";
8571
+ if (this.#attachTray?.hasPending() === true) {
8572
+ this.#appendNotice(
8573
+ "\u{1F4CE}",
8574
+ this.#strings.attachmentsStillUploading.replace(
8575
+ "{n}",
8576
+ String(this.#attachTray.pendingCount())
8577
+ ),
8578
+ "attachment-pending"
8579
+ );
8580
+ }
8581
+ this.#attachTray?.clearReady();
8582
+ await this.sendMessage(content, attachments);
8583
+ }
8584
+ /**
8585
+ * Send a message as if the user had typed it — renders the user bubble,
8586
+ * dispatches {@link SUBMIT_EVENT}, and starts the run.
8587
+ *
8588
+ * The programmatic half of the composer, for a host driving its own input
8589
+ * (a "Ask about this order" button, a command palette, a custom composer
8590
+ * replacing the built-in one). Everything the built-in Send does happens
8591
+ * here; Send itself now reads the composer, clears it, and calls this.
8592
+ *
8593
+ * `attachments` are durable {@link AttachmentRef}s — the shape
8594
+ * {@link attachFile}'s upload resolves to, and the shape
8595
+ * {@link ATTACHMENT_EVENT} reports. Pass them to attach files to the message.
8596
+ *
8597
+ * No-ops while a run is in flight (a second concurrent run would orphan the
8598
+ * first) and for an entirely empty message. Unlike the built-in Send, this
8599
+ * does **not** consult the tray: what you pass is what is sent, so a host
8600
+ * composer stays in charge of its own state.
8601
+ */
8602
+ async sendMessage(content, attachments = []) {
8603
+ if (this.#running || content === "" && attachments.length === 0) {
8604
+ return;
8605
+ }
8341
8606
  const bubble = this.appendMessage(MESSAGE_ROLE.USER, content);
8342
8607
  if (attachments.length > 0) {
8343
8608
  bubble.appendChild(renderAttachmentChips(attachments));
8344
8609
  }
8345
- this.#input.value = "";
8346
- this.#attachTray?.clearReady();
8347
8610
  this.#runAttachments = attachments;
8348
8611
  this.dispatchEvent(
8349
8612
  new CustomEvent(SUBMIT_EVENT, {
@@ -8354,6 +8617,35 @@ Use the read_attachment tool with an id to read a file's contents.`
8354
8617
  );
8355
8618
  await this.#client_send(content, attachments);
8356
8619
  }
8620
+ /**
8621
+ * Queue a file for upload into the attachment tray, exactly as the file
8622
+ * picker and drag-and-drop do — validation, progress chip, and all.
8623
+ *
8624
+ * Returns `false` when uploads are not configured (no `data-attachments-url`
8625
+ * and no {@link uploadHandler}), which is the only way for a host to tell;
8626
+ * the tray does not exist to report anything in that case.
8627
+ *
8628
+ * Uploading is asynchronous: watch {@link ATTACHMENT_EVENT} for the resulting
8629
+ * {@link AttachmentRef}, and pass it to {@link sendMessage} once `pending`
8630
+ * reaches zero.
8631
+ */
8632
+ attachFile(file) {
8633
+ if (this.#attachTray === null) {
8634
+ return false;
8635
+ }
8636
+ this.#attachTray.add(file);
8637
+ return true;
8638
+ }
8639
+ /** Tell the host what the tray now holds — see {@link ATTACHMENT_EVENT}. */
8640
+ #dispatchAttachments(tray) {
8641
+ this.dispatchEvent(
8642
+ new CustomEvent(ATTACHMENT_EVENT, {
8643
+ detail: { attachments: tray.readyRefs(), pending: tray.pendingCount() },
8644
+ bubbles: true,
8645
+ composed: true
8646
+ })
8647
+ );
8648
+ }
8357
8649
  async #client_send(content, attachments) {
8358
8650
  if (this.endpoint === "") {
8359
8651
  return;
@@ -8539,6 +8831,7 @@ Use the read_attachment tool with an id to read a file's contents.`
8539
8831
  if (this.#streamDeltas <= 1) {
8540
8832
  this.#revealWords(bubble);
8541
8833
  }
8834
+ attachCopyButtons(bubble, this.#strings);
8542
8835
  this.#streamingBubble = null;
8543
8836
  },
8544
8837
  onToolCall: (call) => {
@@ -8766,8 +9059,9 @@ function setControlValue(el, value) {
8766
9059
  }
8767
9060
 
8768
9061
  // src/version.ts
8769
- var VERSION = "0.16.0";
9062
+ var VERSION = "0.18.0";
8770
9063
  export {
9064
+ ATTACHMENT_EVENT,
8771
9065
  AgUiChat,
8772
9066
  AgUiClient,
8773
9067
  COMPACTION_ACTIVITY_TYPE,