@artooi/ag-ui-web-component 0.29.0 → 0.30.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +199 -1
  2. package/README.md +196 -33
  3. package/dist/ag-ui-web-component.bundle.js +211 -28
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +60 -1
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +25 -1
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/core/agui_client.d.ts +18 -1
  10. package/dist/core/agui_client.d.ts.map +1 -1
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +578 -35
  14. package/dist/index.js.map +4 -4
  15. package/dist/ui/message_actions.d.ts +14 -4
  16. package/dist/ui/message_actions.d.ts.map +1 -1
  17. package/dist/ui/styles.d.ts +1 -1
  18. package/dist/ui/styles.d.ts.map +1 -1
  19. package/dist/ui/subagent_panel.d.ts +92 -0
  20. package/dist/ui/subagent_panel.d.ts.map +1 -0
  21. package/dist/ui/subagent_update.d.ts +19 -0
  22. package/dist/ui/subagent_update.d.ts.map +1 -0
  23. package/dist/ui/tool_call_card.d.ts +73 -1
  24. package/dist/ui/tool_call_card.d.ts.map +1 -1
  25. package/dist/ui/ui_strings.d.ts +10 -0
  26. package/dist/ui/ui_strings.d.ts.map +1 -1
  27. package/package.json +1 -1
  28. package/src/constants.ts +63 -1
  29. package/src/core/ag_ui_chat.ts +180 -26
  30. package/src/core/agui_client.ts +26 -2
  31. package/src/index.ts +4 -0
  32. package/src/ui/message_actions.ts +20 -8
  33. package/src/ui/styles.ts +183 -0
  34. package/src/ui/subagent_panel.ts +213 -0
  35. package/src/ui/subagent_update.ts +80 -0
  36. package/src/ui/tool_call_card.ts +129 -3
  37. package/src/ui/ui_strings.ts +15 -0
  38. package/src/version.ts +1 -1
package/dist/index.js CHANGED
@@ -11,6 +11,14 @@ var INVALIDATE_EVENT = "ag-ui-invalidate";
11
11
  var FEEDBACK_EVENT = "ag-ui-feedback";
12
12
  var SUGGESTIONS_ACTIVITY_TYPE = "suggestions";
13
13
  var INVALIDATE_CUSTOM_NAME = "ag_ui.invalidate";
14
+ var SUBAGENT_CUSTOM_NAME = "ag_ui.subagent";
15
+ var SUBAGENT_PHASE = {
16
+ STARTED: "started",
17
+ TOOL_CALL: "tool_call",
18
+ TOOL_RESULT: "tool_result",
19
+ FINISHED: "finished",
20
+ FAILED: "failed"
21
+ };
14
22
  var MESSAGE_ROLE = {
15
23
  USER: "user",
16
24
  ASSISTANT: "assistant"
@@ -21,6 +29,11 @@ var X_SUMMARY_KEY = "x-summary";
21
29
  var X_NAVIGATES_KEY = "x-navigates";
22
30
  var READ_PAGE_TOOL = "read_page";
23
31
  var MAX_TOOL_ROUNDS = 10;
32
+ var MESSAGE_ACTIONS = {
33
+ COPY: "copy",
34
+ RETRY: "retry",
35
+ FEEDBACK: "feedback"
36
+ };
24
37
  var TOOL_CALL_STATUS = {
25
38
  PENDING: "pending",
26
39
  DEFERRED: "deferred",
@@ -587,6 +600,8 @@ var DEFAULT_UI_STRINGS = {
587
600
  errorLabel: "Error",
588
601
  declinedLabel: "Declined",
589
602
  details: "Details",
603
+ subAgentWorking: "Working\u2026",
604
+ subAgentSteps: "Steps the sub-agent took",
590
605
  approvalEditArgs: "Edit the arguments before approving",
591
606
  approvalArgsInvalid: "That is not valid JSON, so nothing was sent.",
592
607
  approvalArgsNotAnObject: "Arguments have to be a JSON object.",
@@ -1744,7 +1759,10 @@ function attachMessageActions(bubble, options) {
1744
1759
  return;
1745
1760
  }
1746
1761
  const bar = messageActionBar(bubble, options.strings);
1747
- bar.appendChild(copyButton(options));
1762
+ const text3 = options.text;
1763
+ if (text3 !== void 0) {
1764
+ bar.appendChild(copyButton(options.strings, text3));
1765
+ }
1748
1766
  if (options.onFeedback !== void 0) {
1749
1767
  bar.append(
1750
1768
  feedbackButton("up", options.strings.feedbackUp, options.onFeedback),
@@ -1782,11 +1800,10 @@ function messageActionButton(modifier, label, glyph) {
1782
1800
  button2.appendChild(icon);
1783
1801
  return button2;
1784
1802
  }
1785
- function copyButton(options) {
1786
- const { strings } = options;
1803
+ function copyButton(strings, text3) {
1787
1804
  const button2 = messageActionButton("copy", strings.copyMessage, "\u2398");
1788
1805
  button2.addEventListener("click", () => {
1789
- void navigator.clipboard.writeText(options.text()).then(
1806
+ void navigator.clipboard.writeText(text3()).then(
1790
1807
  () => flash2(button2, strings.copied, strings.copyMessage),
1791
1808
  // A denied clipboard permission is the common case, not an exception:
1792
1809
  // say so on the button rather than throwing into an unhandled rejection.
@@ -6625,6 +6642,21 @@ var STYLES = `
6625
6642
  color: var(--_fg);
6626
6643
  }
6627
6644
 
6645
+ /* A region a host formatter took over, marked by the card. Preformatted
6646
+ whitespace is what makes the built-in block read as written, and it is the one
6647
+ thing a host cannot want: a table inherits it as mangled cell spacing, and a
6648
+ sentence as line breaks nobody typed.
6649
+
6650
+ Whitespace only. The card's own face, frame, padding and scroll cap stay,
6651
+ because the card is one visual object -- the head row and the status pill are
6652
+ monospaced too -- and a region that dropped the family would be the only part
6653
+ of it wearing a different one. A host that wants that restyles the
6654
+ tool-card-result part, which does not need the formatter at all. */
6655
+ .tool-call-args[data-formatted],
6656
+ .tool-call-result[data-formatted] {
6657
+ white-space: normal;
6658
+ }
6659
+
6628
6660
  /* Display modes are pure visibility over one DOM shape, selected from the host
6629
6661
  attribute rather than a value stamped on the card at build time, so flipping
6630
6662
  data-tool-display re-styles cards already on screen. See ToolCallCard.
@@ -6689,6 +6721,174 @@ var STYLES = `
6689
6721
  display: flex;
6690
6722
  }
6691
6723
 
6724
+ /* A delegated sub-agent's progress, inside the card that delegated. Empty on
6725
+ every card that delegated nothing, so it collapses rather than adding a gap
6726
+ to each one -- the same shape the approval slot uses. */
6727
+ .tool-call-subagent:empty {
6728
+ display: none;
6729
+ }
6730
+
6731
+ .tool-call-subagent {
6732
+ display: flex;
6733
+ flex-direction: column;
6734
+ gap: 4px;
6735
+ }
6736
+
6737
+ .subagent {
6738
+ display: flex;
6739
+ flex-direction: column;
6740
+ gap: 4px;
6741
+ min-width: 0;
6742
+ }
6743
+
6744
+ /* The collapsed row is the status and the expander at once, which is what keeps
6745
+ a ten-step child one row until somebody opens it. Full width and left-aligned,
6746
+ because it is a line of the card rather than a button on it. */
6747
+ .subagent-row {
6748
+ display: flex;
6749
+ align-items: center;
6750
+ gap: 6px;
6751
+ width: 100%;
6752
+ box-sizing: border-box;
6753
+ padding: 2px 0;
6754
+ border: none;
6755
+ background: none;
6756
+ font: inherit;
6757
+ text-align: left;
6758
+ color: var(--_muted);
6759
+ cursor: pointer;
6760
+ }
6761
+
6762
+ /* Nothing behind the row yet -- a delegation that failed before calling
6763
+ anything. Drop the affordances rather than offer a control that expands onto
6764
+ an empty region, which is the refusal the card's own toggle already makes. */
6765
+ .subagent-row:disabled {
6766
+ cursor: default;
6767
+ }
6768
+
6769
+ .subagent-row::after {
6770
+ content: "\u25B8";
6771
+ flex: none;
6772
+ margin-left: auto;
6773
+ color: var(--_accent);
6774
+ }
6775
+
6776
+ .subagent-row[aria-expanded="true"]::after {
6777
+ content: "\u25BE";
6778
+ }
6779
+
6780
+ .subagent-row:disabled::after {
6781
+ display: none;
6782
+ }
6783
+
6784
+ /* Empty in the DOM; the glyph is drawn here from the panel's data-phase, so a
6785
+ host re-themes it through the same tool-icon custom properties the card uses. */
6786
+ .subagent-icon {
6787
+ flex: none;
6788
+ box-sizing: border-box;
6789
+ display: inline-flex;
6790
+ align-items: center;
6791
+ justify-content: center;
6792
+ width: 10px;
6793
+ height: 10px;
6794
+ font-size: 10px;
6795
+ line-height: 1;
6796
+ }
6797
+
6798
+ /* Anything that is not a terminal phase is the child still working. Selected by
6799
+ what it is not, so a phase this client has not heard of still spins rather
6800
+ than rendering as a blank. */
6801
+ .subagent[data-phase]:not([data-phase="finished"]):not([data-phase="failed"]) .subagent-icon {
6802
+ border: 2px solid var(--_muted);
6803
+ border-top-color: transparent;
6804
+ border-radius: 50%;
6805
+ animation: ag-ui-tool-spin var(--_tool-spin-duration) linear infinite;
6806
+ }
6807
+
6808
+ .subagent[data-phase="finished"] .subagent-icon::before {
6809
+ content: var(--_tool-icon-done);
6810
+ color: var(--_success);
6811
+ }
6812
+
6813
+ .subagent[data-phase="failed"] .subagent-icon::before {
6814
+ content: var(--_tool-icon-error);
6815
+ color: var(--_danger);
6816
+ }
6817
+
6818
+ @media (prefers-reduced-motion: reduce) {
6819
+ .subagent .subagent-icon {
6820
+ animation: none;
6821
+ }
6822
+ }
6823
+
6824
+ /* The server's own pre-rendered line. Shrinks and wraps rather than pushing the
6825
+ chevron out of the card, which is what a fixed-width sibling in a flex row
6826
+ does to a panel at sidebar width. */
6827
+ .subagent-status {
6828
+ flex: 1 1 auto;
6829
+ min-width: 0;
6830
+ overflow-wrap: anywhere;
6831
+ }
6832
+
6833
+ /* The child's own calls. Indented and ruled, so the nesting is visible without
6834
+ a second card frame around it. */
6835
+ .subagent-steps {
6836
+ display: flex;
6837
+ flex-direction: column;
6838
+ gap: 2px;
6839
+ margin-left: 4px;
6840
+ padding-left: 10px;
6841
+ border-left: 1px solid var(--_border);
6842
+ }
6843
+
6844
+ .subagent-steps[hidden] {
6845
+ display: none;
6846
+ }
6847
+
6848
+ .subagent-step {
6849
+ display: flex;
6850
+ align-items: center;
6851
+ gap: 6px;
6852
+ min-width: 0;
6853
+ color: var(--_muted);
6854
+ }
6855
+
6856
+ .subagent-step-icon {
6857
+ flex: none;
6858
+ box-sizing: border-box;
6859
+ display: inline-flex;
6860
+ align-items: center;
6861
+ justify-content: center;
6862
+ width: 8px;
6863
+ height: 8px;
6864
+ font-size: 9px;
6865
+ line-height: 1;
6866
+ }
6867
+
6868
+ /* No outcome yet: the wire says null while the call is in flight, and the
6869
+ absence of the attribute is how that arrives here. A hollow ring, not a
6870
+ spinner -- several can be on screen at once and the row above already spins. */
6871
+ .subagent-step:not([data-ok]) .subagent-step-icon {
6872
+ border: 1px solid var(--_muted);
6873
+ border-radius: 50%;
6874
+ }
6875
+
6876
+ .subagent-step[data-ok="true"] .subagent-step-icon::before {
6877
+ content: var(--_tool-icon-done);
6878
+ color: var(--_success);
6879
+ }
6880
+
6881
+ .subagent-step[data-ok="false"] .subagent-step-icon::before {
6882
+ content: var(--_tool-icon-error);
6883
+ color: var(--_danger);
6884
+ }
6885
+
6886
+ .subagent-step-name {
6887
+ flex: 1 1 auto;
6888
+ min-width: 0;
6889
+ overflow-wrap: anywhere;
6890
+ }
6891
+
6692
6892
  .tool-call-toggle {
6693
6893
  align-self: flex-start;
6694
6894
  border: none;
@@ -7988,6 +8188,159 @@ var STYLES = `
7988
8188
  }
7989
8189
  `;
7990
8190
 
8191
+ // src/ui/subagent_panel.ts
8192
+ var SubAgentPanel = class {
8193
+ /** The panel's root; append this into the delegating card's slot. */
8194
+ element;
8195
+ /**
8196
+ * The collapsed row, which is the expander as well as the status.
8197
+ *
8198
+ * Disabled while the child has called nothing, so a delegation that failed
8199
+ * before it started offers no control that expands onto an empty region —
8200
+ * the same refusal the card's own Details toggle already makes.
8201
+ */
8202
+ #row;
8203
+ #status;
8204
+ #steps;
8205
+ /** The child's tool calls, keyed by the child's own call id. */
8206
+ #stepRows = /* @__PURE__ */ new Map();
8207
+ constructor(strings = DEFAULT_UI_STRINGS) {
8208
+ this.element = document.createElement("div");
8209
+ this.element.className = "subagent";
8210
+ this.element.setAttribute("part", "subagent");
8211
+ this.#row = document.createElement("button");
8212
+ this.#row.type = "button";
8213
+ this.#row.className = "subagent-row";
8214
+ this.#row.setAttribute("part", "subagent-row");
8215
+ this.#row.setAttribute("aria-expanded", "false");
8216
+ this.#row.disabled = true;
8217
+ const icon = document.createElement("span");
8218
+ icon.className = "subagent-icon";
8219
+ icon.setAttribute("part", "subagent-icon");
8220
+ icon.setAttribute("aria-hidden", "true");
8221
+ this.#status = document.createElement("span");
8222
+ this.#status.className = "subagent-status";
8223
+ this.#status.setAttribute("part", "subagent-status");
8224
+ this.#status.textContent = strings.subAgentWorking;
8225
+ this.#row.append(icon, this.#status);
8226
+ this.#steps = document.createElement("div");
8227
+ this.#steps.className = "subagent-steps";
8228
+ this.#steps.setAttribute("part", "subagent-steps");
8229
+ this.#steps.setAttribute("role", "list");
8230
+ this.#steps.setAttribute("aria-label", strings.subAgentSteps);
8231
+ this.#steps.hidden = true;
8232
+ this.#row.addEventListener("click", () => {
8233
+ this.#setExpanded(this.#row.getAttribute("aria-expanded") !== "true");
8234
+ });
8235
+ this.element.append(this.#row, this.#steps);
8236
+ }
8237
+ /**
8238
+ * Fold one announcement in.
8239
+ *
8240
+ * Every field is applied only when the update actually carried it, so a phase
8241
+ * that says nothing about the agent or the status leaves both as they stand.
8242
+ * That is what lets `finished` be two keys wide on the wire without blanking
8243
+ * the row it closes.
8244
+ */
8245
+ report(update) {
8246
+ this.element.setAttribute("data-phase", update.phase);
8247
+ if (update.agent !== null) {
8248
+ this.element.setAttribute("data-agent", update.agent);
8249
+ }
8250
+ if (update.status !== null) {
8251
+ this.#status.textContent = update.status;
8252
+ }
8253
+ if (update.tool !== null) {
8254
+ this.#recordStep(update.tool);
8255
+ }
8256
+ }
8257
+ /**
8258
+ * Open or settle one of the child's calls, keyed by its own id.
8259
+ *
8260
+ * The absence of `data-ok` is what "still running" looks like, mirroring the
8261
+ * wire's `null` rather than inventing a third value for it — so the attribute
8262
+ * is removed on the way in and written on the way out.
8263
+ */
8264
+ #recordStep(tool) {
8265
+ const row = this.#stepRows.get(tool.toolCallId) ?? this.#createStep(tool);
8266
+ if (tool.ok === null) {
8267
+ row.removeAttribute("data-ok");
8268
+ return;
8269
+ }
8270
+ row.setAttribute("data-ok", String(tool.ok));
8271
+ }
8272
+ #createStep(tool) {
8273
+ const row = document.createElement("div");
8274
+ row.className = "subagent-step";
8275
+ row.setAttribute("part", "subagent-step");
8276
+ row.setAttribute("role", "listitem");
8277
+ row.setAttribute("data-tool-call-id", tool.toolCallId);
8278
+ const icon = document.createElement("span");
8279
+ icon.className = "subagent-step-icon";
8280
+ icon.setAttribute("part", "subagent-step-icon");
8281
+ icon.setAttribute("aria-hidden", "true");
8282
+ const name = document.createElement("span");
8283
+ name.className = "subagent-step-name";
8284
+ name.setAttribute("part", "subagent-step-name");
8285
+ name.textContent = tool.name;
8286
+ row.append(icon, name);
8287
+ this.#steps.appendChild(row);
8288
+ this.#stepRows.set(tool.toolCallId, row);
8289
+ this.#row.disabled = false;
8290
+ return row;
8291
+ }
8292
+ #setExpanded(expanded) {
8293
+ this.#steps.hidden = !expanded;
8294
+ this.#row.setAttribute("aria-expanded", String(expanded));
8295
+ }
8296
+ };
8297
+
8298
+ // src/ui/subagent_update.ts
8299
+ var PHASES = Object.values(SUBAGENT_PHASE);
8300
+ function asRecord(value) {
8301
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
8302
+ return null;
8303
+ }
8304
+ return value;
8305
+ }
8306
+ function asText(value) {
8307
+ return typeof value === "string" && value !== "" ? value : null;
8308
+ }
8309
+ function asTool(value) {
8310
+ const record = asRecord(value);
8311
+ if (record === null) {
8312
+ return null;
8313
+ }
8314
+ const toolCallId = asText(record["toolCallId"]);
8315
+ const name = asText(record["name"]);
8316
+ const ok = record["ok"];
8317
+ if (toolCallId === null || name === null) {
8318
+ return null;
8319
+ }
8320
+ if (ok !== null && typeof ok !== "boolean") {
8321
+ return null;
8322
+ }
8323
+ return { toolCallId, name, ok };
8324
+ }
8325
+ function subAgentUpdate(value) {
8326
+ const record = asRecord(value);
8327
+ if (record === null) {
8328
+ return null;
8329
+ }
8330
+ const delegationId = asText(record["delegationId"]);
8331
+ const phase = record["phase"];
8332
+ if (delegationId === null || typeof phase !== "string" || !PHASES.includes(phase)) {
8333
+ return null;
8334
+ }
8335
+ return {
8336
+ delegationId,
8337
+ phase,
8338
+ agent: asText(record["agent"]),
8339
+ status: asText(record["status"]),
8340
+ tool: asTool(record["tool"])
8341
+ };
8342
+ }
8343
+
7991
8344
  // src/ui/suggestion_chips.ts
7992
8345
  var MAX_SUGGESTIONS = 4;
7993
8346
  var MAX_SUGGESTION_CHARS = 120;
@@ -8394,6 +8747,22 @@ var ToolCallCard = class {
8394
8747
  * Empty until used, and hidden while empty by the shadow CSS.
8395
8748
  */
8396
8749
  approvalSlot;
8750
+ /**
8751
+ * Where a *nested* run's progress renders — the sub-agent this call delegated
8752
+ * to, narrating itself while the card waits.
8753
+ *
8754
+ * A slot rather than a rendered thing, on the same reasoning as
8755
+ * {@link approvalSlot}: the card owns the position and something else owns the
8756
+ * content. What makes the position right is that the wire keys a delegation on
8757
+ * this card's own `toolCallId`, so the run being narrated is the one this card
8758
+ * already stands for.
8759
+ *
8760
+ * Placed above the Details toggle rather than inside the body, because the
8761
+ * body is what the display modes hide — and a progress line that only appears
8762
+ * in `full` mode would leave exactly the stall it exists to end. Empty on every
8763
+ * card that delegated nothing, and hidden while empty by the shadow CSS.
8764
+ */
8765
+ subagentSlot;
8397
8766
  #status;
8398
8767
  #decision;
8399
8768
  #toggle;
@@ -8409,10 +8778,15 @@ var ToolCallCard = class {
8409
8778
  * still exist when the user is asked to approve, edit or deny the call.
8410
8779
  */
8411
8780
  args;
8781
+ /** The raw tool name, kept for the payloads handed to {@link ToolPayloadFormatter}. */
8782
+ #name;
8783
+ #formatPayload;
8412
8784
  #settled = false;
8413
- constructor(name, args, summary, strings = DEFAULT_UI_STRINGS) {
8785
+ constructor(name, args, summary, strings = DEFAULT_UI_STRINGS, options = {}) {
8414
8786
  this.#strings = strings;
8415
8787
  this.args = args;
8788
+ this.#name = name;
8789
+ this.#formatPayload = options.formatPayload ?? null;
8416
8790
  this.element = document.createElement("div");
8417
8791
  this.element.className = "tool-call";
8418
8792
  this.element.setAttribute("part", "tool-card");
@@ -8440,7 +8814,11 @@ var ToolCallCard = class {
8440
8814
  this.#decision.hidden = true;
8441
8815
  head.append(icon, label, this.#status, this.#decision);
8442
8816
  const argsSection = this.#section("args", strings.argumentsLabel);
8443
- argsSection.body.textContent = JSON.stringify(args, null, 2);
8817
+ this.#renderPayload(
8818
+ argsSection.body,
8819
+ { kind: "arguments", toolName: name, args },
8820
+ JSON.stringify(args, null, 2)
8821
+ );
8444
8822
  argsSection.root.hidden = Object.keys(args).length === 0;
8445
8823
  const resultSection = this.#section("result", strings.resultLabel);
8446
8824
  this.#resultSection = resultSection.root;
@@ -8461,7 +8839,10 @@ var ToolCallCard = class {
8461
8839
  this.approvalSlot = document.createElement("div");
8462
8840
  this.approvalSlot.className = "tool-call-approval";
8463
8841
  this.approvalSlot.setAttribute("part", "tool-card-approval");
8464
- this.element.append(head, this.#toggle, body, this.approvalSlot);
8842
+ this.subagentSlot = document.createElement("div");
8843
+ this.subagentSlot.className = "tool-call-subagent";
8844
+ this.subagentSlot.setAttribute("part", "tool-card-subagent");
8845
+ this.element.append(head, this.subagentSlot, this.#toggle, body, this.approvalSlot);
8465
8846
  }
8466
8847
  /**
8467
8848
  * Move between the two states that are not an outcome — `pending` (running)
@@ -8503,9 +8884,42 @@ var ToolCallCard = class {
8503
8884
  this.element.setAttribute("data-status", status);
8504
8885
  this.#status.textContent = statusLabels(this.#strings)[status];
8505
8886
  this.#resultLabel.textContent = resultLabels(this.#strings)[status];
8506
- this.#resultBody.textContent = formatPayload(text3);
8887
+ this.#renderPayload(
8888
+ this.#resultBody,
8889
+ { kind: "result", toolName: this.#name, status, text: text3 },
8890
+ formatPayload(text3)
8891
+ );
8507
8892
  this.#resultSection.hidden = false;
8508
8893
  }
8894
+ /**
8895
+ * Fill one body region, offering it to the host formatter first.
8896
+ *
8897
+ * `fallback` is computed by the caller rather than here because the two
8898
+ * regions build it differently -- arguments are already parsed, a result is a
8899
+ * string that may or may not be JSON -- and because a formatter that takes
8900
+ * the region over should not have paid for a pretty-print nobody sees. It is
8901
+ * cheap either way; the point is that the built-in rendering stays written
8902
+ * once, beside the payload it belongs to.
8903
+ *
8904
+ * The `data-formatted` marker is what the shadow CSS reads to relax the
8905
+ * preformatted whitespace on a region a host owns -- a table inherits it as
8906
+ * mangled cell spacing, a sentence as line breaks nobody typed. Marked for a
8907
+ * returned string too: one rule, and a summary line wants ordinary wrapping
8908
+ * as much as a table does.
8909
+ */
8910
+ #renderPayload(body, payload, fallback) {
8911
+ const rendered = this.#formatPayload === null ? null : this.#formatPayload(payload);
8912
+ if (rendered === null) {
8913
+ body.textContent = fallback;
8914
+ return;
8915
+ }
8916
+ body.setAttribute("data-formatted", "true");
8917
+ if (typeof rendered === "string") {
8918
+ body.textContent = rendered;
8919
+ return;
8920
+ }
8921
+ body.replaceChildren(rendered);
8922
+ }
8509
8923
  /** Build one labelled region of the body: a heading plus a payload block. */
8510
8924
  #section(kind, labelText) {
8511
8925
  const root = document.createElement("div");
@@ -8709,6 +9123,7 @@ var AgUiClient = class {
8709
9123
  */
8710
9124
  #closedMessageIds = /* @__PURE__ */ new Set();
8711
9125
  #connectionLostMessage;
9126
+ #maxToolRounds;
8712
9127
  // Set by cancel(); reset at the top of each #run(). Checked by the loop so
8713
9128
  // a cancel between frontend-tool rounds doesn't start another round.
8714
9129
  #cancelled = false;
@@ -8722,6 +9137,8 @@ var AgUiClient = class {
8722
9137
  this.#onPersist = config.onPersist ?? (() => {
8723
9138
  });
8724
9139
  this.#connectionLostMessage = config.connectionLostMessage ?? "Connection lost";
9140
+ const rounds = config.maxToolRounds ?? MAX_TOOL_ROUNDS;
9141
+ this.#maxToolRounds = rounds >= 1 ? Math.floor(rounds) : MAX_TOOL_ROUNDS;
8725
9142
  const onStateChanged = config.onStateChanged;
8726
9143
  if (onStateChanged !== void 0) {
8727
9144
  this.#agent.subscribe({
@@ -8752,7 +9169,8 @@ var AgUiClient = class {
8752
9169
  *
8753
9170
  * When the agent calls frontend tools, this executes them and re-runs the
8754
9171
  * agent with the results, looping until the agent stops calling frontend
8755
- * tools (bounded by {@link MAX_TOOL_ROUNDS}).
9172
+ * tools (bounded by {@link AgUiClientConfig.maxToolRounds}, which defaults to
9173
+ * {@link MAX_TOOL_ROUNDS}).
8756
9174
  *
8757
9175
  * `attachments` ride on the user message as a non-standard field so the
8758
9176
  * default store round-trips them for history replay; see
@@ -8849,7 +9267,7 @@ var AgUiClient = class {
8849
9267
  }
8850
9268
  async #runLoop() {
8851
9269
  let resume;
8852
- for (let round = 0; round < MAX_TOOL_ROUNDS; round += 1) {
9270
+ for (let round = 0; round < this.#maxToolRounds; round += 1) {
8853
9271
  if (this.#cancelled) {
8854
9272
  return;
8855
9273
  }
@@ -9870,6 +10288,30 @@ var AgUiChat = class extends HTMLElement {
9870
10288
  * schema never reaches the browser. Client tools should prefer `x-summary`.
9871
10289
  */
9872
10290
  toolSummaries = {};
10291
+ /**
10292
+ * Optional presentation hook for the two payload regions of a tool-call card
10293
+ * -- the arguments and the result. Unset (the default) leaves both
10294
+ * pretty-printed as JSON.
10295
+ *
10296
+ * The seam exists because a wide result has no good rendering as JSON: a
10297
+ * thirty-field row is a wall of text where the host wanted a table, or a
10298
+ * sentence. `ClientTool.render` cannot answer it -- it is handed the
10299
+ * *arguments* only, and a server-side tool has no `ClientTool` at all, so the
10300
+ * result region was the one part of the transcript a host could not reach.
10301
+ *
10302
+ * **Presentation, not translation.** The card and the model already read
10303
+ * separate copies of a tool result: the model's is maintained by
10304
+ * `@ag-ui/client` from the same event and persisted with the history, and the
10305
+ * card has always shown that string reformatted. So a formatter changes what
10306
+ * the person reads and nothing the agent reads -- which makes restyling safe
10307
+ * and *rewording* a way to make the card disagree with the prose beside it.
10308
+ * Rename a value on the server, where it reaches both.
10309
+ *
10310
+ * Read at render time rather than captured, so a host that sets it from a
10311
+ * framework effect after the first card still formats the results that settle
10312
+ * afterwards. See {@link ToolPayloadFormatter}.
10313
+ */
10314
+ formatToolPayload = null;
9873
10315
  /**
9874
10316
  * Localizable UI strings — a partial override merged over the English
9875
10317
  * {@link DEFAULT_UI_STRINGS}. Resolved once on connect (so set it before the
@@ -9919,6 +10361,16 @@ var AgUiChat = class extends HTMLElement {
9919
10361
  #toolRegistry = new ClientToolRegistry();
9920
10362
  /** Tool-call cards awaiting execution, keyed by call id. */
9921
10363
  #toolCards = /* @__PURE__ */ new Map();
10364
+ /**
10365
+ * The live delegation panels, keyed by the **parent's** `delegate_task` call
10366
+ * id — which is what the wire keys a sub-agent's progress on, so this map and
10367
+ * {@link #toolCards} answer to the same key.
10368
+ *
10369
+ * Kept beside the cards rather than on them, so a card stays a card: the tool
10370
+ * card holds the slot and this holds what went into it, the same division the
10371
+ * approval prompt already uses.
10372
+ */
10373
+ #subagentPanels = /* @__PURE__ */ new Map();
9922
10374
  /**
9923
10375
  * Call ids whose card was already settled from a streamed server-side result
9924
10376
  * (`TOOL_CALL_RESULT`), so the post-run executeTool sweep doesn't overwrite
@@ -10910,6 +11362,37 @@ ${quoted}`;
10910
11362
  this.#quoteButton.hidden = true;
10911
11363
  this.#quoting = "";
10912
11364
  }
11365
+ /**
11366
+ * The tool-round budget from `data-max-tool-rounds`, for one send.
11367
+ *
11368
+ * Anything unparseable becomes `NaN`, which {@link AgUiClient} rejects along
11369
+ * with a bound below one -- so the two ways of setting this are validated in
11370
+ * one place rather than agreeing by coincidence.
11371
+ */
11372
+ #maxToolRounds() {
11373
+ const attr = this.getAttribute("data-max-tool-rounds");
11374
+ return attr === null ? MAX_TOOL_ROUNDS : Number.parseInt(attr, 10);
11375
+ }
11376
+ /**
11377
+ * Which message actions a finished bubble offers, from
11378
+ * `data-message-actions`.
11379
+ *
11380
+ * Absent means all of them, so the attribute only ever subtracts: the row
11381
+ * shipped without an off switch and a host that never sets this must keep
11382
+ * exactly what it had. A value names the survivors, which makes
11383
+ * `data-message-actions="false"` -- the spelling its sibling
11384
+ * `data-quote-selection` uses -- an empty set by falling out of the same rule
11385
+ * rather than by a case of its own.
11386
+ */
11387
+ #messageActions() {
11388
+ const attr = this.getAttribute("data-message-actions");
11389
+ if (attr === null) {
11390
+ return new Set(Object.values(MESSAGE_ACTIONS));
11391
+ }
11392
+ return new Set(
11393
+ attr.split(",").map((token) => token.trim()).filter((token) => token !== "")
11394
+ );
11395
+ }
10913
11396
  /** The client-side upload size cap from `data-attachment-max-bytes`. */
10914
11397
  #attachmentMaxBytes() {
10915
11398
  const attr = this.getAttribute("data-attachment-max-bytes");
@@ -11455,6 +11938,7 @@ ${quoted}`;
11455
11938
  this.#thoughts = null;
11456
11939
  this.#hidePending();
11457
11940
  this.#toolCards.clear();
11941
+ this.#subagentPanels.clear();
11458
11942
  this.#serverSettled.clear();
11459
11943
  this.#cardElements.clear();
11460
11944
  this.#activityBlocks.clear();
@@ -12277,7 +12761,8 @@ ${quoted}`;
12277
12761
  resolveInterrupts: (interrupts) => this.#resolveInterrupts(interrupts),
12278
12762
  onPersist: (messages) => this.conversationStore.saveMessages(this.#threadId, messages),
12279
12763
  onStateChanged: (state) => this.#onSharedStateChanged(state),
12280
- connectionLostMessage: this.#strings.connectionLost
12764
+ connectionLostMessage: this.#strings.connectionLost,
12765
+ maxToolRounds: this.#maxToolRounds()
12281
12766
  });
12282
12767
  }
12283
12768
  return this.#client;
@@ -12299,30 +12784,39 @@ ${quoted}`;
12299
12784
  * Every finished bubble gets copy and feedback -- both are safe on a message
12300
12785
  * of any age. Retry moves to the newest, because it is the only one where
12301
12786
  * re-running answers the same question rather than rewriting history.
12787
+ *
12788
+ * `data-message-actions` subtracts from that. The row is built only when
12789
+ * something survives to go in it: an empty row still takes its margin, still
12790
+ * answers to the `message-actions` part, and still reads to a screen reader
12791
+ * as a group of actions with none in it.
12302
12792
  */
12303
12793
  #attachActions(bubble, options = {}) {
12304
- attachMessageActions(bubble, {
12305
- strings: this.#strings,
12306
- // Read at click time, not captured: a bubble rendered from markdown holds
12307
- // its text in the DOM, and that is what the user sees and means to copy.
12308
- text: () => bubble.textContent,
12309
- // A failed run is copyable -- error text is what people paste into a bug
12310
- // report -- but not rateable: a rating is a statement about an *answer*,
12311
- // and mixing "the connection dropped" into that signal makes the host's
12312
- // feedback data say less than it did before.
12313
- ...options.rateable === false ? {} : {
12314
- onFeedback: (rating) => {
12315
- this.dispatchEvent(
12316
- new CustomEvent(FEEDBACK_EVENT, {
12317
- detail: { content: bubble.textContent, rating },
12318
- bubbles: true,
12319
- composed: true
12320
- })
12321
- );
12322
- }
12323
- }
12324
- });
12325
- this.#moveRetryTo(messageActionBar(bubble, this.#strings));
12794
+ const enabled = this.#messageActions();
12795
+ const copyable = enabled.has(MESSAGE_ACTIONS.COPY);
12796
+ const rateable = options.rateable !== false && enabled.has(MESSAGE_ACTIONS.FEEDBACK);
12797
+ if (copyable || rateable) {
12798
+ attachMessageActions(bubble, {
12799
+ strings: this.#strings,
12800
+ // Read at click time, not captured: a bubble rendered from markdown
12801
+ // holds its text in the DOM, and that is what the user sees and means
12802
+ // to copy.
12803
+ ...copyable ? { text: () => bubble.textContent } : {},
12804
+ ...rateable ? {
12805
+ onFeedback: (rating) => {
12806
+ this.dispatchEvent(
12807
+ new CustomEvent(FEEDBACK_EVENT, {
12808
+ detail: { content: bubble.textContent, rating },
12809
+ bubbles: true,
12810
+ composed: true
12811
+ })
12812
+ );
12813
+ }
12814
+ } : {}
12815
+ });
12816
+ }
12817
+ if (enabled.has(MESSAGE_ACTIONS.RETRY)) {
12818
+ this.#moveRetryTo(messageActionBar(bubble, this.#strings));
12819
+ }
12326
12820
  }
12327
12821
  /** Move the Retry button onto `bar`, taking it off whoever held it. */
12328
12822
  #moveRetryTo(bar) {
@@ -12559,6 +13053,10 @@ ${quoted}`;
12559
13053
  this.#dispatchInvalidation(value);
12560
13054
  return;
12561
13055
  }
13056
+ if (name === SUBAGENT_CUSTOM_NAME) {
13057
+ this.#reportSubAgent(value);
13058
+ return;
13059
+ }
12562
13060
  this.dispatchEvent(
12563
13061
  new CustomEvent(CUSTOM_AGENT_EVENT, {
12564
13062
  detail: { name, value },
@@ -12681,6 +13179,45 @@ ${quoted}`;
12681
13179
  })
12682
13180
  );
12683
13181
  }
13182
+ /**
13183
+ * Draw one step of a delegated sub-agent's progress, on the card that
13184
+ * delegated.
13185
+ *
13186
+ * `delegationId` is the parent's own `delegate_task` tool-call id, so the
13187
+ * attachment point is a card this element already drew on `TOOL_CALL_START`.
13188
+ * That is the whole design: a run that hands work to a sub-agent used to read
13189
+ * as a stall -- the card sat at "running…" for the child's entire duration --
13190
+ * and the fix is to narrate *into* the thing that was already standing there,
13191
+ * rather than to float a second element with the same identity.
13192
+ *
13193
+ * A progress event for a call this client never drew is dropped. It has no
13194
+ * card to attach to, and inventing a floating one is precisely the alternative
13195
+ * that was rejected: parent and child interleave in the transcript with
13196
+ * nothing marking whose is whose, and the persisted transcript -- which never
13197
+ * held the progress at all -- would not match what was on screen.
13198
+ *
13199
+ * Nothing here writes to the conversation store. `CUSTOM` never enters
13200
+ * `agent.messages`, so a reload mid-run leaves the tool card and loses the
13201
+ * nested detail, which is the intended behaviour rather than a gap.
13202
+ */
13203
+ #reportSubAgent(value) {
13204
+ const update = subAgentUpdate(value);
13205
+ if (update === null) {
13206
+ return;
13207
+ }
13208
+ const card = this.#toolCards.get(update.delegationId);
13209
+ if (card === void 0) {
13210
+ return;
13211
+ }
13212
+ let panel = this.#subagentPanels.get(update.delegationId);
13213
+ if (panel === void 0) {
13214
+ panel = new SubAgentPanel(this.#strings);
13215
+ this.#subagentPanels.set(update.delegationId, panel);
13216
+ card.subagentSlot.appendChild(panel.element);
13217
+ }
13218
+ panel.report(update);
13219
+ this.#scroller.follow();
13220
+ }
12684
13221
  /** A muted "⏹ Stopped" line in the transcript (distinct from the ⚠️ error bubble). */
12685
13222
  #appendStoppedNote() {
12686
13223
  const note = document.createElement("div");
@@ -13017,7 +13554,12 @@ ${quoted}`;
13017
13554
  }
13018
13555
  const labelled = this.#resolveTool(call.name)?.parameters[X_SUMMARY_KEY];
13019
13556
  const summary = typeof labelled === "string" ? labelled : this.toolSummaries[call.name] ?? this.#toolCatalog[call.name]?.summary ?? prettifyToolName(call.name);
13020
- const card = new ToolCallCard(call.name, call.args, summary, this.#strings);
13557
+ const card = new ToolCallCard(call.name, call.args, summary, this.#strings, {
13558
+ // A thunk over the live property, not the property itself: the card keeps
13559
+ // this for the life of the call, and the result region is filled when the
13560
+ // tool settles -- which can be long after a host set the hook.
13561
+ formatPayload: (payload) => this.formatToolPayload?.(payload) ?? null
13562
+ });
13021
13563
  this.#toolCards.set(call.id, card);
13022
13564
  this.#ensureGroup().appendChild(card.element);
13023
13565
  this.#updateEmptyState();
@@ -13091,7 +13633,7 @@ function setControlValue(el2, value) {
13091
13633
  }
13092
13634
 
13093
13635
  // src/version.ts
13094
- var VERSION = "0.29.0";
13636
+ var VERSION = "0.30.0";
13095
13637
  export {
13096
13638
  ATTACHMENT_EVENT,
13097
13639
  AgUiChat,
@@ -13113,6 +13655,7 @@ export {
13113
13655
  MAX_SUGGESTIONS,
13114
13656
  MAX_SUGGESTION_CHARS,
13115
13657
  MAX_TOOL_ROUNDS,
13658
+ MESSAGE_ACTIONS,
13116
13659
  MESSAGE_ROLE,
13117
13660
  PAGE_ACTIONS,
13118
13661
  RUN_FINISHED_EVENT,