@artooi/ag-ui-web-component 0.17.0 → 0.19.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
@@ -450,6 +450,9 @@ var DEFAULT_UI_STRINGS = {
450
450
  collapse: "Collapse",
451
451
  expand: "Expand",
452
452
  toggleTheme: "Toggle theme",
453
+ copyCode: "Copy",
454
+ copied: "Copied",
455
+ copyFailed: "Copy failed",
453
456
  checkpoints: "Continue a run",
454
457
  noCheckpoints: "Nothing to continue yet.",
455
458
  resumeRun: "Resume",
@@ -482,6 +485,9 @@ var DEFAULT_UI_STRINGS = {
482
485
  toolDone: "\u2713 done",
483
486
  toolError: "\u26A0 error",
484
487
  toolDeclined: "\u2298 declined",
488
+ decisionApproved: "approved by you",
489
+ decisionDeclined: "declined by you",
490
+ argumentsLabel: "Arguments",
485
491
  resultLabel: "Result",
486
492
  errorLabel: "Error",
487
493
  declinedLabel: "Declined",
@@ -531,12 +537,12 @@ function mergeUiStrings(overrides) {
531
537
 
532
538
  // src/ui/approval_card.ts
533
539
  function actionButton(modifier, label) {
534
- const button = document.createElement("button");
535
- button.type = "button";
536
- button.className = `approval-btn approval-btn--${modifier}`;
537
- button.setAttribute("part", `approval-button approval-${modifier}`);
538
- button.textContent = label;
539
- return button;
540
+ const button2 = document.createElement("button");
541
+ button2.type = "button";
542
+ button2.className = `approval-btn approval-btn--${modifier}`;
543
+ button2.setAttribute("part", `approval-button approval-${modifier}`);
544
+ button2.textContent = label;
545
+ return button2;
540
546
  }
541
547
  function requestApproval(host, request, options = {}) {
542
548
  const strings = options.strings ?? DEFAULT_UI_STRINGS;
@@ -583,6 +589,55 @@ function requestApproval(host, request, options = {}) {
583
589
  });
584
590
  }
585
591
 
592
+ // src/ui/attach_copy_buttons.ts
593
+ var CONFIRM_MS = 1500;
594
+ function attachCopyButtons(root, strings) {
595
+ for (const pre of Array.from(root.querySelectorAll("pre"))) {
596
+ const code = pre.querySelector("code");
597
+ if (code === null || pre.querySelector(".code-copy") !== null) {
598
+ continue;
599
+ }
600
+ pre.classList.add("has-copy");
601
+ pre.append(button(code, strings));
602
+ }
603
+ }
604
+ function button(code, strings) {
605
+ const text2 = code.textContent;
606
+ const copy = document.createElement("button");
607
+ copy.type = "button";
608
+ copy.className = "code-copy";
609
+ copy.setAttribute("part", "code-copy");
610
+ copy.textContent = strings.copyCode;
611
+ copy.title = strings.copyCode;
612
+ copy.setAttribute("aria-label", strings.copyCode);
613
+ copy.addEventListener("click", () => {
614
+ void writeText(text2).then((ok) => {
615
+ confirm(copy, ok ? strings.copied : strings.copyFailed, strings);
616
+ });
617
+ });
618
+ return copy;
619
+ }
620
+ async function writeText(text2) {
621
+ const clipboard = navigator.clipboard;
622
+ if (clipboard === void 0) {
623
+ return false;
624
+ }
625
+ try {
626
+ await clipboard.writeText(text2);
627
+ return true;
628
+ } catch {
629
+ return false;
630
+ }
631
+ }
632
+ function confirm(copy, message, strings) {
633
+ copy.textContent = message;
634
+ copy.dataset["state"] = message === strings.copied ? "copied" : "failed";
635
+ setTimeout(() => {
636
+ copy.textContent = strings.copyCode;
637
+ delete copy.dataset["state"];
638
+ }, CONFIRM_MS);
639
+ }
640
+
586
641
  // src/ui/attachment_chips.ts
587
642
  function renderAttachmentChips(refs) {
588
643
  const list = document.createElement("div");
@@ -885,6 +940,8 @@ var CheckpointMenu = class {
885
940
  #onPick;
886
941
  #list;
887
942
  #heading;
943
+ /** What had focus before the panel opened, restored on close. */
944
+ #lastFocused = null;
888
945
  #strings;
889
946
  #runs = [];
890
947
  constructor(onPick, strings = DEFAULT_UI_STRINGS) {
@@ -895,6 +952,7 @@ var CheckpointMenu = class {
895
952
  this.element.setAttribute("part", "checkpoints");
896
953
  this.element.setAttribute("role", "dialog");
897
954
  this.element.setAttribute("aria-label", strings.checkpoints);
955
+ this.element.tabIndex = -1;
898
956
  this.element.hidden = true;
899
957
  const header = document.createElement("div");
900
958
  header.className = "checkpoints-header";
@@ -908,12 +966,7 @@ var CheckpointMenu = class {
908
966
  this.#list.className = "checkpoints-list";
909
967
  this.#list.setAttribute("part", "checkpoints-list");
910
968
  this.element.append(header, this.#list);
911
- this.element.addEventListener("keydown", (event) => {
912
- if (event.key === "Escape") {
913
- event.stopPropagation();
914
- this.close();
915
- }
916
- });
969
+ this.element.addEventListener("keydown", (event) => this.#onKeydown(event));
917
970
  }
918
971
  /** Replace the rows. The host passes only `continuable` runs. */
919
972
  setRuns(runs) {
@@ -928,10 +981,52 @@ var CheckpointMenu = class {
928
981
  this.#render();
929
982
  }
930
983
  open() {
984
+ if (this.open_) {
985
+ return;
986
+ }
987
+ this.#lastFocused = this.#activeElement();
931
988
  this.element.hidden = false;
989
+ (this.#focusables()[0] ?? this.element).focus();
932
990
  }
933
991
  close() {
992
+ if (!this.open_) {
993
+ return;
994
+ }
934
995
  this.element.hidden = true;
996
+ this.#lastFocused?.focus();
997
+ this.#lastFocused = null;
998
+ }
999
+ /** The focused element within this panel's root (shadow-aware). */
1000
+ #activeElement() {
1001
+ return this.element.getRootNode().activeElement;
1002
+ }
1003
+ /** The panel's tabbable controls, in document order. */
1004
+ #focusables() {
1005
+ return Array.from(this.element.querySelectorAll("button, [tabindex]")).filter(
1006
+ (el) => !el.hidden
1007
+ );
1008
+ }
1009
+ /** Escape closes; Tab is trapped so focus cannot wander behind the dialog. */
1010
+ #onKeydown(event) {
1011
+ if (event.key === "Escape") {
1012
+ event.stopPropagation();
1013
+ this.close();
1014
+ return;
1015
+ }
1016
+ if (event.key !== "Tab") {
1017
+ return;
1018
+ }
1019
+ const focusables = this.#focusables();
1020
+ const first = focusables[0];
1021
+ const last = focusables[focusables.length - 1];
1022
+ const active = this.#activeElement();
1023
+ if (event.shiftKey && active === first) {
1024
+ event.preventDefault();
1025
+ last?.focus();
1026
+ } else if (!event.shiftKey && active === last) {
1027
+ event.preventDefault();
1028
+ first?.focus();
1029
+ }
935
1030
  }
936
1031
  get open_() {
937
1032
  return !this.element.hidden;
@@ -975,27 +1070,27 @@ var CheckpointMenu = class {
975
1070
  return row;
976
1071
  }
977
1072
  #action(runId, verb, label) {
978
- const button = document.createElement("button");
979
- button.type = "button";
980
- button.className = `checkpoint-action checkpoint-${verb}`;
981
- button.setAttribute("part", `checkpoint-action checkpoint-${verb}`);
982
- button.textContent = label;
983
- button.addEventListener("click", () => {
1073
+ const button2 = document.createElement("button");
1074
+ button2.type = "button";
1075
+ button2.className = `checkpoint-action checkpoint-${verb}`;
1076
+ button2.setAttribute("part", `checkpoint-action checkpoint-${verb}`);
1077
+ button2.textContent = label;
1078
+ button2.addEventListener("click", () => {
984
1079
  this.close();
985
1080
  this.#onPick(runId, verb);
986
1081
  });
987
- return button;
1082
+ return button2;
988
1083
  }
989
1084
  };
990
1085
 
991
1086
  // src/ui/confirmation_card.ts
992
1087
  function actionButton2(modifier, label) {
993
- const button = document.createElement("button");
994
- button.type = "button";
995
- button.className = `confirm-btn confirm-btn--${modifier}`;
996
- button.setAttribute("part", `confirm-button confirm-${modifier}`);
997
- button.textContent = label;
998
- return button;
1088
+ const button2 = document.createElement("button");
1089
+ button2.type = "button";
1090
+ button2.className = `confirm-btn confirm-btn--${modifier}`;
1091
+ button2.setAttribute("part", `confirm-button confirm-${modifier}`);
1092
+ button2.textContent = label;
1093
+ return button2;
999
1094
  }
1000
1095
  function requestConfirmation(host, request, options = {}) {
1001
1096
  const strings = options.strings ?? DEFAULT_UI_STRINGS;
@@ -1014,33 +1109,32 @@ function requestConfirmation(host, request, options = {}) {
1014
1109
  args.className = "confirm-args";
1015
1110
  args.setAttribute("part", "confirm-args");
1016
1111
  args.textContent = JSON.stringify(request.args, null, 2);
1112
+ args.hidden = Object.keys(request.args).length === 0;
1017
1113
  const actions = document.createElement("div");
1018
1114
  actions.className = "confirm-actions";
1019
1115
  actions.setAttribute("part", "confirm-actions");
1020
1116
  const cancel = actionButton2("cancel", strings.cancel);
1021
- const confirm = actionButton2("confirm", strings.confirm);
1117
+ const confirm2 = actionButton2("confirm", strings.confirm);
1022
1118
  let settled = false;
1023
1119
  const close = (accepted) => {
1024
1120
  if (settled) {
1025
1121
  return;
1026
1122
  }
1027
1123
  settled = true;
1028
- cancel.disabled = true;
1029
- confirm.disabled = true;
1030
- card.setAttribute("data-resolved", accepted ? "confirmed" : "declined");
1124
+ card.remove();
1031
1125
  resolve(accepted);
1032
1126
  };
1033
1127
  cancel.addEventListener("click", () => close(false));
1034
- confirm.addEventListener("click", () => close(true));
1128
+ confirm2.addEventListener("click", () => close(true));
1035
1129
  options.signal?.addEventListener("abort", () => close(false), { once: true });
1036
- actions.append(cancel, confirm);
1130
+ actions.append(cancel, confirm2);
1037
1131
  card.append(body, args, actions);
1038
1132
  host.appendChild(card);
1039
1133
  if (options.signal?.aborted === true) {
1040
1134
  close(false);
1041
1135
  return;
1042
1136
  }
1043
- confirm.focus();
1137
+ confirm2.focus();
1044
1138
  });
1045
1139
  }
1046
1140
 
@@ -4193,13 +4287,13 @@ var SkillsMenu = class {
4193
4287
  const chipSkills = this.#chipsEnabled ? this.#skills.filter((skill) => skill.chip === true) : [];
4194
4288
  this.chips.hidden = chipSkills.length === 0;
4195
4289
  for (const skill of chipSkills) {
4196
- const button = document.createElement("button");
4197
- button.type = "button";
4198
- button.className = "skill-chip";
4199
- button.setAttribute("part", "skill-chip");
4200
- button.textContent = skill.title;
4201
- button.addEventListener("click", () => this.#pick(skill));
4202
- this.chips.appendChild(button);
4290
+ const button2 = document.createElement("button");
4291
+ button2.type = "button";
4292
+ button2.className = "skill-chip";
4293
+ button2.setAttribute("part", "skill-chip");
4294
+ button2.textContent = skill.title;
4295
+ button2.addEventListener("click", () => this.#pick(skill));
4296
+ this.chips.appendChild(button2);
4203
4297
  }
4204
4298
  }
4205
4299
  #renderPalette() {
@@ -4740,6 +4834,52 @@ var STYLES = `
4740
4834
  padding: 0;
4741
4835
  }
4742
4836
 
4837
+ /* The copy button sits inside the block, so it scrolls with wide code rather
4838
+ than floating over the bubble. Positioning is on the pre; the button only
4839
+ appears once one has been attached. */
4840
+ .message--assistant pre.has-copy {
4841
+ position: relative;
4842
+ }
4843
+
4844
+ .code-copy {
4845
+ position: absolute;
4846
+ top: 4px;
4847
+ right: 4px;
4848
+ padding: 2px 8px;
4849
+ font: inherit;
4850
+ font-size: 0.75em;
4851
+ line-height: 1.6;
4852
+ color: var(--ag-ui-muted);
4853
+ background: var(--ag-ui-surface);
4854
+ border: 1px solid var(--ag-ui-border);
4855
+ border-radius: 4px;
4856
+ cursor: pointer;
4857
+ opacity: 0;
4858
+ transition: opacity 0.12s ease;
4859
+ }
4860
+
4861
+ /* Revealed on hover or keyboard focus. Focus matters as much as hover here:
4862
+ hidden-until-hover is invisible to a keyboard user otherwise. */
4863
+ .message--assistant pre.has-copy:hover .code-copy,
4864
+ .code-copy:focus-visible {
4865
+ opacity: 1;
4866
+ }
4867
+
4868
+ .code-copy:hover {
4869
+ color: var(--ag-ui-text);
4870
+ background: var(--ag-ui-hover);
4871
+ }
4872
+
4873
+ .code-copy[data-state="copied"] {
4874
+ opacity: 1;
4875
+ color: var(--ag-ui-text);
4876
+ }
4877
+
4878
+ .code-copy[data-state="failed"] {
4879
+ opacity: 1;
4880
+ color: var(--ag-ui-danger, var(--ag-ui-text));
4881
+ }
4882
+
4743
4883
  .message--assistant blockquote {
4744
4884
  padding-left: 10px;
4745
4885
  border-left: 3px solid var(--ag-ui-border);
@@ -4954,10 +5094,9 @@ var STYLES = `
4954
5094
  }
4955
5095
  }
4956
5096
 
4957
- /* Inline display mode: the lightest card \u2014 drop the box chrome so the
4958
- status row reads as one line of the answer; the result toggle still expands
4959
- below it. */
4960
- .tool-call[data-display="inline"] {
5097
+ /* Inline display mode: the lightest card. Drop the box chrome so the status row
5098
+ reads as one line of the answer; the result toggle still expands below it. */
5099
+ :host([data-tool-display="inline"]) .tool-call {
4961
5100
  max-width: 100%;
4962
5101
  background: transparent;
4963
5102
  border: none;
@@ -4987,6 +5126,37 @@ var STYLES = `
4987
5126
  color: var(--ag-ui-muted);
4988
5127
  }
4989
5128
 
5129
+ .tool-call-body {
5130
+ display: flex;
5131
+ flex-direction: column;
5132
+ gap: 6px;
5133
+ }
5134
+
5135
+ .tool-call-section {
5136
+ display: flex;
5137
+ flex-direction: column;
5138
+ gap: 3px;
5139
+ }
5140
+
5141
+ /* The heading that tells the two payloads apart. Without it the arguments and
5142
+ the result were one run of text and a reader had to guess the boundary. */
5143
+ .tool-call-section-label {
5144
+ font-size: 10px;
5145
+ font-weight: 700;
5146
+ letter-spacing: 0.06em;
5147
+ text-transform: uppercase;
5148
+ color: var(--ag-ui-muted);
5149
+ }
5150
+
5151
+ /* The record of a human decision on a gated call. An approved call used to
5152
+ look exactly like one that was never gated. */
5153
+ .tool-call-decision {
5154
+ flex: none;
5155
+ font-size: 11px;
5156
+ font-style: italic;
5157
+ color: var(--ag-ui-muted);
5158
+ }
5159
+
4990
5160
  .tool-call-args,
4991
5161
  .tool-call-result {
4992
5162
  margin: 0;
@@ -5001,6 +5171,43 @@ var STYLES = `
5001
5171
  color: var(--ag-ui-fg);
5002
5172
  }
5003
5173
 
5174
+ /* Display modes are pure visibility over one DOM shape, selected from the host
5175
+ attribute rather than a value stamped on the card when it was built. That is
5176
+ what lets a host flip data-tool-display and have every card already on screen
5177
+ re-read it, the way data-answer-well behaves. Baking the structure per mode
5178
+ meant the setting only reached cards created afterwards.
5179
+
5180
+ Default (no attribute) is the full mode: arguments always visible, result
5181
+ behind the toggle. */
5182
+ .tool-call[data-expanded="false"] .tool-call-section--result {
5183
+ display: none;
5184
+ }
5185
+
5186
+ /* Compact: one toggle over both regions, so a settled card is a single line
5187
+ until asked. */
5188
+ :host([data-tool-display="compact"]) .tool-call[data-expanded="false"] .tool-call-section {
5189
+ display: none;
5190
+ }
5191
+
5192
+ /* Inline: the result only; the call's arguments are noise at this density. */
5193
+ :host([data-tool-display="inline"]) .tool-call .tool-call-section--args {
5194
+ display: none;
5195
+ }
5196
+
5197
+ /* Minimal: the status row and nothing else, so there is no toggle to press. */
5198
+ :host([data-tool-display="minimal"]) .tool-call .tool-call-toggle,
5199
+ :host([data-tool-display="minimal"]) .tool-call .tool-call-body {
5200
+ display: none;
5201
+ }
5202
+
5203
+ /* A pending card has no result yet, and in the modes where the arguments are
5204
+ hidden too there is nothing behind the toggle. Hide the control rather than
5205
+ offer one that expands onto nothing. */
5206
+ .tool-call[data-status="pending"] .tool-call-toggle,
5207
+ :host([data-tool-display="inline"]) .tool-call[data-status="pending"] .tool-call-toggle {
5208
+ display: none;
5209
+ }
5210
+
5004
5211
  .tool-call-toggle {
5005
5212
  align-self: flex-start;
5006
5213
  border: none;
@@ -6053,9 +6260,9 @@ var ThreadDrawer = class {
6053
6260
  }
6054
6261
  /** Swap a row for an inline "Delete? [Delete] [Cancel]" confirm. */
6055
6262
  #confirmDelete(row, meta) {
6056
- const confirm = document.createElement("div");
6057
- confirm.className = "drawer-confirm";
6058
- confirm.setAttribute("part", "drawer-confirm");
6263
+ const confirm2 = document.createElement("div");
6264
+ confirm2.className = "drawer-confirm";
6265
+ confirm2.setAttribute("part", "drawer-confirm");
6059
6266
  const label = document.createElement("span");
6060
6267
  label.className = "drawer-confirm-label";
6061
6268
  label.setAttribute("part", "drawer-confirm-label");
@@ -6072,8 +6279,8 @@ var ThreadDrawer = class {
6072
6279
  no.setAttribute("part", "drawer-confirm-no");
6073
6280
  no.textContent = this.#strings.cancel;
6074
6281
  no.addEventListener("click", () => this.#renderList());
6075
- confirm.append(label, yes, no);
6076
- row.replaceChildren(confirm);
6282
+ confirm2.append(label, yes, no);
6283
+ row.replaceChildren(confirm2);
6077
6284
  }
6078
6285
  };
6079
6286
 
@@ -6093,24 +6300,32 @@ function resultLabels(strings) {
6093
6300
  [TOOL_CALL_STATUS.DECLINED]: strings.declinedLabel
6094
6301
  };
6095
6302
  }
6303
+ function formatPayload(text2) {
6304
+ try {
6305
+ return JSON.stringify(JSON.parse(text2), null, 2);
6306
+ } catch {
6307
+ return text2;
6308
+ }
6309
+ }
6096
6310
  var ToolCallCard = class {
6097
6311
  /** The card's root element; append this into the message list. */
6098
6312
  element;
6099
6313
  #status;
6100
- #mode;
6101
- #args;
6314
+ #decision;
6315
+ #toggle;
6316
+ #resultSection;
6317
+ #resultLabel;
6318
+ #resultBody;
6102
6319
  #strings;
6103
6320
  #settled = false;
6104
- constructor(name, args, mode = TOOL_DISPLAY.FULL, summary, strings = DEFAULT_UI_STRINGS) {
6105
- this.#mode = mode;
6106
- this.#args = args;
6321
+ constructor(name, args, summary, strings = DEFAULT_UI_STRINGS) {
6107
6322
  this.#strings = strings;
6108
6323
  this.element = document.createElement("div");
6109
6324
  this.element.className = "tool-call";
6110
6325
  this.element.setAttribute("part", "tool-card");
6111
6326
  this.element.setAttribute("data-tool-name", name);
6112
6327
  this.element.setAttribute("data-status", TOOL_CALL_STATUS.PENDING);
6113
- this.element.setAttribute("data-display", mode);
6328
+ this.element.setAttribute("data-expanded", "false");
6114
6329
  const head = document.createElement("div");
6115
6330
  head.className = "tool-call-head";
6116
6331
  head.setAttribute("part", "tool-card-head");
@@ -6126,24 +6341,52 @@ var ToolCallCard = class {
6126
6341
  this.#status.className = "tool-call-status";
6127
6342
  this.#status.setAttribute("part", "tool-card-status");
6128
6343
  this.#status.textContent = statusLabels(strings)[TOOL_CALL_STATUS.PENDING];
6129
- head.append(icon, label, this.#status);
6130
- this.element.append(head);
6131
- if (mode === TOOL_DISPLAY.FULL) {
6132
- const argsEl = document.createElement("pre");
6133
- argsEl.className = "tool-call-args";
6134
- argsEl.setAttribute("part", "tool-card-args");
6135
- argsEl.textContent = JSON.stringify(args, null, 2);
6136
- this.element.append(argsEl);
6137
- }
6344
+ this.#decision = document.createElement("span");
6345
+ this.#decision.className = "tool-call-decision";
6346
+ this.#decision.setAttribute("part", "tool-card-decision");
6347
+ this.#decision.hidden = true;
6348
+ head.append(icon, label, this.#status, this.#decision);
6349
+ const argsSection = this.#section("args", strings.argumentsLabel);
6350
+ argsSection.body.textContent = JSON.stringify(args, null, 2);
6351
+ argsSection.root.hidden = Object.keys(args).length === 0;
6352
+ const resultSection = this.#section("result", strings.resultLabel);
6353
+ this.#resultSection = resultSection.root;
6354
+ this.#resultLabel = resultSection.label;
6355
+ this.#resultBody = resultSection.body;
6356
+ resultSection.root.hidden = true;
6357
+ this.#toggle = document.createElement("button");
6358
+ this.#toggle.type = "button";
6359
+ this.#toggle.className = "tool-call-toggle";
6360
+ this.#toggle.setAttribute("part", "tool-card-toggle");
6361
+ this.#toggle.setAttribute("aria-expanded", "false");
6362
+ this.#toggle.textContent = strings.details;
6363
+ this.#toggle.addEventListener("click", () => this.#setExpanded(!this.#expanded()));
6364
+ const body = document.createElement("div");
6365
+ body.className = "tool-call-body";
6366
+ body.setAttribute("part", "tool-card-body");
6367
+ body.append(argsSection.root, resultSection.root);
6368
+ this.element.append(head, this.#toggle, body);
6369
+ }
6370
+ /**
6371
+ * Record that a human approved or declined this call, as a line in the card.
6372
+ *
6373
+ * Approval used to leave no trace at all: a declined call became a tool
6374
+ * result saying so, while an approved one simply ran, making the transcript
6375
+ * of a gated call byte-identical to one that was never gated. The prompt is
6376
+ * gone once answered, so this is where the decision lives.
6377
+ */
6378
+ recordDecision(kind) {
6379
+ this.element.setAttribute("data-decision", kind);
6380
+ this.#decision.textContent = kind === "approved" ? this.#strings.decisionApproved : this.#strings.decisionDeclined;
6381
+ this.#decision.hidden = false;
6138
6382
  }
6139
6383
  /** Whether {@link settle} has already run (so a terminal sweep can skip it). */
6140
6384
  get settled() {
6141
6385
  return this.#settled;
6142
6386
  }
6143
6387
  /**
6144
- * Flip the status pill to ``status`` and, unless in `minimal` mode, append a
6145
- * collapsed body behind a click-to-expand toggle: the result alone (`full` /
6146
- * `inline`), or the args + result together (`compact`).
6388
+ * Flip the status pill to `status` and fill in the result region, whose
6389
+ * heading names the outcome (result / error / declined).
6147
6390
  */
6148
6391
  settle(status, text2) {
6149
6392
  if (this.#settled) {
@@ -6152,33 +6395,31 @@ var ToolCallCard = class {
6152
6395
  this.#settled = true;
6153
6396
  this.element.setAttribute("data-status", status);
6154
6397
  this.#status.textContent = statusLabels(this.#strings)[status];
6155
- if (this.#mode === TOOL_DISPLAY.MINIMAL) {
6156
- return;
6157
- }
6158
- const toggle = document.createElement("button");
6159
- toggle.type = "button";
6160
- toggle.className = "tool-call-toggle";
6161
- toggle.setAttribute("part", "tool-card-toggle");
6162
- toggle.setAttribute("aria-expanded", "false");
6163
- const output = document.createElement("pre");
6164
- output.className = "tool-call-result";
6165
- output.setAttribute("part", "tool-card-result");
6166
- output.hidden = true;
6167
- if (this.#mode === TOOL_DISPLAY.COMPACT) {
6168
- toggle.textContent = this.#strings.details;
6169
- output.textContent = `args: ${JSON.stringify(this.#args)}
6170
-
6171
- ${text2}`;
6172
- } else {
6173
- toggle.textContent = resultLabels(this.#strings)[status];
6174
- output.textContent = text2;
6175
- }
6176
- toggle.addEventListener("click", () => {
6177
- const expand = output.hidden;
6178
- output.hidden = !expand;
6179
- toggle.setAttribute("aria-expanded", String(expand));
6180
- });
6181
- this.element.append(toggle, output);
6398
+ this.#resultLabel.textContent = resultLabels(this.#strings)[status];
6399
+ this.#resultBody.textContent = formatPayload(text2);
6400
+ this.#resultSection.hidden = false;
6401
+ }
6402
+ /** Build one labelled region of the body: a heading plus a payload block. */
6403
+ #section(kind, labelText) {
6404
+ const root = document.createElement("div");
6405
+ root.className = `tool-call-section tool-call-section--${kind}`;
6406
+ root.setAttribute("part", `tool-card-section tool-card-${kind}-section`);
6407
+ const label = document.createElement("span");
6408
+ label.className = "tool-call-section-label";
6409
+ label.setAttribute("part", `tool-card-section-label tool-card-${kind}-label`);
6410
+ label.textContent = labelText;
6411
+ const body = document.createElement("pre");
6412
+ body.className = `tool-call-${kind}`;
6413
+ body.setAttribute("part", `tool-card-${kind}`);
6414
+ root.append(label, body);
6415
+ return { root, label, body };
6416
+ }
6417
+ #expanded() {
6418
+ return this.element.getAttribute("data-expanded") === "true";
6419
+ }
6420
+ #setExpanded(expand) {
6421
+ this.element.setAttribute("data-expanded", String(expand));
6422
+ this.#toggle.setAttribute("aria-expanded", String(expand));
6182
6423
  }
6183
6424
  };
6184
6425
 
@@ -7593,7 +7834,11 @@ var AgUiChat = class extends HTMLElement {
7593
7834
  }
7594
7835
  /**
7595
7836
  * How much detail tool-call cards show, from the `data-tool-display`
7596
- * attribute (`minimal` / `compact` / `full`). Defaults to `full`.
7837
+ * attribute (`minimal` / `inline` / `compact` / `full`). Defaults to `full`.
7838
+ *
7839
+ * Applied by the shadow CSS from the attribute itself, so changing it
7840
+ * restyles every card already in the transcript rather than only the ones
7841
+ * built afterwards.
7597
7842
  */
7598
7843
  get toolDisplay() {
7599
7844
  const attr = this.getAttribute("data-tool-display");
@@ -7647,6 +7892,22 @@ var AgUiChat = class extends HTMLElement {
7647
7892
  this.#attachTray?.dispose();
7648
7893
  this.#voice?.dispose();
7649
7894
  }
7895
+ /**
7896
+ * Read an opt-in flag attribute the way HTML reads a boolean attribute.
7897
+ *
7898
+ * Present means on — bare (`data-prompt-chips`), empty
7899
+ * (`data-prompt-chips=""`) or any value except the literal `"false"`. These
7900
+ * were compared against the string `"true"`, so writing the attribute bare —
7901
+ * the spelling every native boolean attribute uses, and the one a reader
7902
+ * reaches for first — silently *disabled* the feature it names, with nothing
7903
+ * to indicate why the chips never appeared.
7904
+ *
7905
+ * `="false"` still turns it off, so an explicit opt-out keeps working.
7906
+ */
7907
+ #flag(name) {
7908
+ const value = this.getAttribute(name);
7909
+ return value !== null && value !== "false";
7910
+ }
7650
7911
  /** Parse the inline `data-strings` JSON overrides (empty when absent/malformed). */
7651
7912
  #readStringOverrides() {
7652
7913
  const raw = this.getAttribute("data-strings");
@@ -7827,8 +8088,8 @@ Use the read_attachment tool with an id to read a file's contents.`
7827
8088
  }
7828
8089
  /** Wire the skill surfaces: opt-in flags, embedded catalog, optional fetch. */
7829
8090
  #initSkills() {
7830
- this.#skillsMenu.enableChips(this.getAttribute("data-prompt-chips") === "true");
7831
- this.#skillsMenu.enableSlash(this.getAttribute("data-slash-commands") === "true");
8091
+ this.#skillsMenu.enableChips(this.#flag("data-prompt-chips"));
8092
+ this.#skillsMenu.enableSlash(this.#flag("data-slash-commands"));
7832
8093
  this.#embedSkills = this.#readEmbeddedSkills();
7833
8094
  this.#recomputeSkills();
7834
8095
  void this.#fetchSkills();
@@ -8169,6 +8430,7 @@ Use the read_attachment tool with an id to read a file's contents.`
8169
8430
  bubble.setAttribute("part", `message message-${role}`);
8170
8431
  if (role === MESSAGE_ROLE.ASSISTANT) {
8171
8432
  bubble.innerHTML = renderMarkdown(content, { allowImages: this.allowImages });
8433
+ attachCopyButtons(bubble, this.#strings);
8172
8434
  this.#ensureGroup().appendChild(bubble);
8173
8435
  } else {
8174
8436
  this.#currentGroup = null;
@@ -8323,16 +8585,29 @@ Use the read_attachment tool with an id to read a file's contents.`
8323
8585
  this.#rail.addEventListener("click", () => this.setCollapsed(false));
8324
8586
  this.#root.append(style, this.#chat, this.#rail);
8325
8587
  }
8326
- /** Build a header control button (icon glyph + localized title/aria). */
8588
+ /**
8589
+ * Build a header control button: a named slot a host can project markup into,
8590
+ * with the built-in glyph as the slot's fallback.
8591
+ *
8592
+ * The glyph used to be the button's own `textContent`, which left a host able
8593
+ * to restyle the control through its `part` but unable to replace it — a CSS
8594
+ * `content` override could swap one character for another, and nothing could
8595
+ * supply a brand `<img>` or `<svg>`. This is the same slot-with-fallback
8596
+ * idiom the header icon already uses, so existing embeds render exactly as
8597
+ * before.
8598
+ */
8327
8599
  #headerButton(modifier, label, glyph) {
8328
- const button = document.createElement("button");
8329
- button.type = "button";
8330
- button.className = `header-btn header-btn--${modifier}`;
8331
- button.setAttribute("part", `header-button ${modifier}-button`);
8332
- button.title = label;
8333
- button.setAttribute("aria-label", label);
8334
- button.textContent = glyph;
8335
- return button;
8600
+ const button2 = document.createElement("button");
8601
+ button2.type = "button";
8602
+ button2.className = `header-btn header-btn--${modifier}`;
8603
+ button2.setAttribute("part", `header-button ${modifier}-button`);
8604
+ button2.title = label;
8605
+ button2.setAttribute("aria-label", label);
8606
+ const slot = document.createElement("slot");
8607
+ slot.name = `icon-${modifier}`;
8608
+ slot.append(document.createTextNode(glyph));
8609
+ button2.append(slot);
8610
+ return button2;
8336
8611
  }
8337
8612
  /**
8338
8613
  * An icon holder wrapping a `<slot>` so a host can project custom markup; with
@@ -8571,7 +8846,7 @@ Use the read_attachment tool with an id to read a file's contents.`
8571
8846
  request.message = confirmText;
8572
8847
  }
8573
8848
  this.#confirmAbort = new AbortController();
8574
- const decision = requestConfirmation(this.#messages, request, {
8849
+ const decision = requestConfirmation(this.#ensureGroup(), request, {
8575
8850
  signal: this.#confirmAbort.signal,
8576
8851
  strings: this.#strings
8577
8852
  });
@@ -8579,6 +8854,7 @@ Use the read_attachment tool with an id to read a file's contents.`
8579
8854
  this.#messages.scrollTop = this.#messages.scrollHeight;
8580
8855
  const accepted = await decision;
8581
8856
  this.#confirmAbort = null;
8857
+ card.recordDecision(accepted ? "approved" : "declined");
8582
8858
  if (!accepted) {
8583
8859
  const message = this.#strings.declinedAction;
8584
8860
  card.settle(TOOL_CALL_STATUS.DECLINED, message);
@@ -8676,6 +8952,7 @@ Use the read_attachment tool with an id to read a file's contents.`
8676
8952
  if (this.#streamDeltas <= 1) {
8677
8953
  this.#revealWords(bubble);
8678
8954
  }
8955
+ attachCopyButtons(bubble, this.#strings);
8679
8956
  this.#streamingBubble = null;
8680
8957
  },
8681
8958
  onToolCall: (call) => {
@@ -8843,7 +9120,7 @@ Use the read_attachment tool with an id to read a file's contents.`
8843
9120
  }
8844
9121
  const labelled = this.#resolveTool(call.name)?.parameters[X_SUMMARY_KEY];
8845
9122
  const summary = typeof labelled === "string" ? labelled : this.toolSummaries[call.name] ?? this.#toolCatalog[call.name] ?? prettifyToolName(call.name);
8846
- const card = new ToolCallCard(call.name, call.args, this.toolDisplay, summary, this.#strings);
9123
+ const card = new ToolCallCard(call.name, call.args, summary, this.#strings);
8847
9124
  this.#toolCards.set(call.id, card);
8848
9125
  this.#ensureGroup().appendChild(card.element);
8849
9126
  this.#updateEmptyState();
@@ -8903,7 +9180,7 @@ function setControlValue(el, value) {
8903
9180
  }
8904
9181
 
8905
9182
  // src/version.ts
8906
- var VERSION = "0.17.0";
9183
+ var VERSION = "0.19.0";
8907
9184
  export {
8908
9185
  ATTACHMENT_EVENT,
8909
9186
  AgUiChat,