@artooi/ag-ui-web-component 0.45.0 → 0.46.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 (56) hide show
  1. package/CHANGELOG.md +60 -1
  2. package/README.md +64 -9
  3. package/dist/ag-ui-web-component.bundle.js +384 -147
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +4 -1
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +10 -5
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/index.js +609 -33
  10. package/dist/index.js.map +4 -4
  11. package/dist/tools/tool_catalog.d.ts +5 -0
  12. package/dist/tools/tool_catalog.d.ts.map +1 -1
  13. package/dist/tools/tool_dispatch.d.ts.map +1 -1
  14. package/dist/ui/interrupts/confirmation_styles.d.ts.map +1 -1
  15. package/dist/ui/placement/surface_for.d.ts +12 -1
  16. package/dist/ui/placement/surface_for.d.ts.map +1 -1
  17. package/dist/ui/progress/thoughts_styles.d.ts.map +1 -1
  18. package/dist/ui/progress/tool_call_card.d.ts +8 -0
  19. package/dist/ui/progress/tool_call_card.d.ts.map +1 -1
  20. package/dist/ui/progress/tool_group_styles.d.ts +2 -0
  21. package/dist/ui/progress/tool_group_styles.d.ts.map +1 -0
  22. package/dist/ui/progress/tool_step_group.d.ts +47 -0
  23. package/dist/ui/progress/tool_step_group.d.ts.map +1 -0
  24. package/dist/ui/progress/tool_step_styles.d.ts.map +1 -1
  25. package/dist/ui/shell/token_styles.d.ts.map +1 -1
  26. package/dist/ui/styles.d.ts.map +1 -1
  27. package/dist/ui/themes/ledger_styles.d.ts.map +1 -1
  28. package/dist/ui/themes/soft_styles.d.ts.map +1 -1
  29. package/dist/ui/transcript/groups_tool_steps.d.ts +13 -0
  30. package/dist/ui/transcript/groups_tool_steps.d.ts.map +1 -0
  31. package/dist/ui/transcript/message_styles.d.ts.map +1 -1
  32. package/dist/ui/transcript/transcript.d.ts +16 -1
  33. package/dist/ui/transcript/transcript.d.ts.map +1 -1
  34. package/dist/ui/ui_strings.d.ts +18 -0
  35. package/dist/ui/ui_strings.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/constants.ts +4 -1
  38. package/src/core/ag_ui_chat.ts +14 -6
  39. package/src/tools/tool_catalog.ts +23 -10
  40. package/src/tools/tool_dispatch.ts +26 -11
  41. package/src/ui/interrupts/confirmation_styles.ts +0 -2
  42. package/src/ui/placement/surface_for.ts +12 -1
  43. package/src/ui/progress/thoughts_styles.ts +0 -2
  44. package/src/ui/progress/tool_call_card.ts +11 -0
  45. package/src/ui/progress/tool_group_styles.ts +232 -0
  46. package/src/ui/progress/tool_step_group.ts +269 -0
  47. package/src/ui/progress/tool_step_styles.ts +0 -2
  48. package/src/ui/shell/token_styles.ts +2 -0
  49. package/src/ui/styles.ts +5 -0
  50. package/src/ui/themes/ledger_styles.ts +15 -0
  51. package/src/ui/themes/soft_styles.ts +20 -0
  52. package/src/ui/transcript/groups_tool_steps.ts +16 -0
  53. package/src/ui/transcript/message_styles.ts +0 -2
  54. package/src/ui/transcript/transcript.ts +109 -3
  55. package/src/ui/ui_strings.ts +31 -0
  56. package/src/version.ts +1 -1
package/dist/index.js CHANGED
@@ -55,6 +55,7 @@ var ATTACHMENT_STATUS = {
55
55
  };
56
56
  var DEFAULT_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
57
57
  var TOOL_DISPLAY = {
58
+ GROUPED: "grouped",
58
59
  INLINE: "inline",
59
60
  MINIMAL: "minimal",
60
61
  COMPACT: "compact",
@@ -1014,6 +1015,13 @@ var DEFAULT_UI_STRINGS = {
1014
1015
  declinedLabel: "Declined",
1015
1016
  interruptedLabel: "Not finished",
1016
1017
  details: "Details",
1018
+ toolGroupSteps: "{count} steps",
1019
+ toolGroupWorking: "Working through {count} steps",
1020
+ toolGroupWorked: "Worked through {count} steps",
1021
+ toolGroupWaiting: "{count} waiting for you",
1022
+ toolGroupFailed: "{count} failed",
1023
+ toolGroupDeclined: "{count} declined",
1024
+ toolStepRepeat: "({n})",
1017
1025
  subAgentWorking: "Working\u2026",
1018
1026
  subAgentDelegatedTo: "Delegated to {agent}",
1019
1027
  subAgentFinished: "{agent} finished",
@@ -1447,14 +1455,19 @@ var ToolCatalog = class {
1447
1455
  });
1448
1456
  const renderer = this.#host.askUserRenderer();
1449
1457
  let answer;
1450
- if (renderer === null) {
1451
- answer = await builtIn();
1452
- } else {
1453
- try {
1454
- answer = await renderer.call(this.#host.element, request, { signal });
1455
- } catch (error) {
1456
- answer = await this.#afterRendererFailed(error, signal, callId, builtIn);
1458
+ this.#host.setAsking(callId, true);
1459
+ try {
1460
+ if (renderer === null) {
1461
+ answer = await builtIn();
1462
+ } else {
1463
+ try {
1464
+ answer = await renderer.call(this.#host.element, request, { signal });
1465
+ } catch (error) {
1466
+ answer = await this.#afterRendererFailed(error, signal, callId, builtIn);
1467
+ }
1457
1468
  }
1469
+ } finally {
1470
+ this.#host.setAsking(callId, false);
1458
1471
  }
1459
1472
  this.#host.decision.close();
1460
1473
  this.#host.updateEmptyState();
@@ -1752,6 +1765,7 @@ var ToolDispatch = class {
1752
1765
  const card = this.#host.transcript.cardFor(call);
1753
1766
  this.#host.transcript.forgetCard(call.id);
1754
1767
  this.#host.transcript.setCardElement(call.id, card.element);
1768
+ this.#host.transcript.holdRunning(call.id, card);
1755
1769
  const tool = this.#host.tools.wasAdvertised(call.name) ? this.#host.tools.resolve(call.name) : null;
1756
1770
  if (tool === null) {
1757
1771
  if (!this.#host.transcript.isServerSettled(call.id)) {
@@ -1784,15 +1798,21 @@ var ToolDispatch = class {
1784
1798
  request.message = confirmText;
1785
1799
  }
1786
1800
  const signal = this.#host.decision.open();
1787
- const decision = requestConfirmation(this.#host.transcript.ensureGroup(), request, {
1788
- signal,
1789
- strings: this.#host.strings(),
1790
- // Offered only where it can be honoured -- see `#confirmationRule`.
1791
- ...rule === "destructive" ? { onAlwaysAllow: () => this.#sessionApproved.add(call.name) } : {}
1792
- });
1793
- this.#host.transcript.updateEmptyState();
1794
- this.#host.transcript.follow();
1795
- const accepted = await decision;
1801
+ card.setAsking(true);
1802
+ let accepted;
1803
+ try {
1804
+ const decision = requestConfirmation(this.#host.transcript.ensureGroup(), request, {
1805
+ signal,
1806
+ strings: this.#host.strings(),
1807
+ // Offered only where it can be honoured -- see `#confirmationRule`.
1808
+ ...rule === "destructive" ? { onAlwaysAllow: () => this.#sessionApproved.add(call.name) } : {}
1809
+ });
1810
+ this.#host.transcript.updateEmptyState();
1811
+ this.#host.transcript.follow();
1812
+ accepted = await decision;
1813
+ } finally {
1814
+ card.setAsking(false);
1815
+ }
1796
1816
  this.#host.decision.close();
1797
1817
  if (this.#host.tenure() !== tenure) {
1798
1818
  return null;
@@ -1893,6 +1913,9 @@ var ToolDispatch = class {
1893
1913
  request.args = card.args;
1894
1914
  }
1895
1915
  card?.mark(TOOL_CALL_STATUS.DEFERRED);
1916
+ if (card !== void 0) {
1917
+ this.#host.transcript.revealStep(card);
1918
+ }
1896
1919
  const builtIn = () => requestApproval(card?.approvalSlot ?? this.#host.transcript.ensureGroup(), request, {
1897
1920
  signal,
1898
1921
  strings: this.#host.strings(),
@@ -6956,6 +6979,16 @@ var ToolCallCard = class {
6956
6979
  this.element.setAttribute("data-status", status);
6957
6980
  this.#status.textContent = statusLabels(this.#strings)[status];
6958
6981
  }
6982
+ /**
6983
+ * Say whether a prompt is open on this call, a question it asked or a
6984
+ * confirmation it needs. A flag of its own beside the status, which stays
6985
+ * `pending`: the call is still running as far as the status goes, and a card
6986
+ * display mode that reads the status draws it exactly as before. The step
6987
+ * group reads the flag to tell a call waiting on a person from one working.
6988
+ */
6989
+ setAsking(asking) {
6990
+ this.element.toggleAttribute("data-asking", asking);
6991
+ }
6959
6992
  /**
6960
6993
  * Record that a human approved or declined this call, as a line in the card.
6961
6994
  * The prompt disappears once answered, so this is the only lasting trace that
@@ -8038,8 +8071,6 @@ var CONFIRMATION_STYLES = `
8038
8071
  padding: 8px 10px;
8039
8072
  max-height: 140px;
8040
8073
  overflow: auto;
8041
- /* Scrolling past the end of this must not scroll the page behind it. */
8042
- overscroll-behavior: contain;
8043
8074
  font-size: 12px;
8044
8075
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
8045
8076
  background: var(--_assistant-bg);
@@ -9057,8 +9088,6 @@ var THOUGHTS_STYLES = `
9057
9088
  border-left: 2px solid var(--_border);
9058
9089
  max-height: 220px;
9059
9090
  overflow: auto;
9060
- /* Scrolling past the end of this must not scroll the page behind it. */
9061
- overscroll-behavior: contain;
9062
9091
  white-space: pre-wrap;
9063
9092
  word-break: break-word;
9064
9093
  font-family: inherit;
@@ -9075,6 +9104,225 @@ var THOUGHTS_STYLES = `
9075
9104
  }
9076
9105
  `;
9077
9106
 
9107
+ // src/ui/progress/tool_group_styles.ts
9108
+ var CARD_MODES = [
9109
+ '[data-tool-display="inline"]',
9110
+ '[data-tool-display="minimal"]',
9111
+ '[data-tool-display="compact"]',
9112
+ '[data-tool-display="full"]'
9113
+ ].join(", ");
9114
+ var GROUPED = `:host(:where(:not(${CARD_MODES})))`;
9115
+ var CARDS = `:host(:is(${CARD_MODES}))`;
9116
+ var TOOL_GROUP_STYLES = `
9117
+ /* \u2500\u2500 The folded tool-step group \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
9118
+ One quiet row per run of consecutive calls, the cards one click down as
9119
+ plain lines rather than boxes. Every rule is scoped to grouped mode, so
9120
+ none of it reaches a group drawn earlier once the host moves to a card
9121
+ mode. */
9122
+ ${GROUPED} .tool-group {
9123
+ align-self: stretch;
9124
+ display: flex;
9125
+ flex-direction: column;
9126
+ min-width: 0;
9127
+ }
9128
+
9129
+ ${GROUPED} .tool-group-head {
9130
+ appearance: none;
9131
+ align-self: flex-start;
9132
+ max-width: 100%;
9133
+ display: inline-flex;
9134
+ align-items: center;
9135
+ gap: 8px;
9136
+ margin: 0 0 0 -6px;
9137
+ padding: 4px 6px;
9138
+ border: 0;
9139
+ border-radius: 6px;
9140
+ background: transparent;
9141
+ color: var(--_muted);
9142
+ font: inherit;
9143
+ font-size: var(--_text-sm);
9144
+ line-height: 1.3;
9145
+ text-align: left;
9146
+ cursor: pointer;
9147
+ }
9148
+
9149
+ ${GROUPED} .tool-group-head:hover {
9150
+ background: var(--_hover);
9151
+ color: var(--_fg);
9152
+ }
9153
+
9154
+ ${GROUPED} .tool-group-head:focus-visible {
9155
+ outline: 2px solid var(--_ring);
9156
+ outline-offset: 1px;
9157
+ }
9158
+
9159
+ ${GROUPED} .tool-group-label {
9160
+ min-width: 0;
9161
+ font-weight: 500;
9162
+ color: var(--_ink-2);
9163
+ overflow-wrap: anywhere;
9164
+ }
9165
+
9166
+ ${GROUPED} .tool-group-meta {
9167
+ min-width: 0;
9168
+ color: var(--_muted);
9169
+ overflow-wrap: anywhere;
9170
+ }
9171
+
9172
+ /* Each note opens with a middle dot, which also parts the first note from
9173
+ the label. */
9174
+ ${GROUPED} .tool-group-note::before {
9175
+ content: "\\00B7";
9176
+ margin: 0 6px 0 2px;
9177
+ }
9178
+
9179
+ ${GROUPED} .tool-group[data-state="error"] .tool-group-meta {
9180
+ color: var(--_danger);
9181
+ }
9182
+
9183
+ ${GROUPED} .tool-group-icon {
9184
+ flex: none;
9185
+ box-sizing: border-box;
9186
+ width: 14px;
9187
+ height: 14px;
9188
+ background: currentColor;
9189
+ mask: var(--_ico-check) center / contain no-repeat;
9190
+ color: var(--_success);
9191
+ }
9192
+
9193
+ ${GROUPED} .tool-group[data-state="error"] .tool-group-icon {
9194
+ mask-image: var(--_ico-error);
9195
+ color: var(--_danger);
9196
+ }
9197
+
9198
+ ${GROUPED} .tool-group[data-state="running"] .tool-group-icon {
9199
+ mask: none;
9200
+ background: none;
9201
+ border: 2px solid var(--_muted);
9202
+ border-top-color: transparent;
9203
+ border-radius: 50%;
9204
+ animation: ag-ui-tool-spin var(--_tool-spin-duration) linear infinite;
9205
+ }
9206
+
9207
+ /* Waiting on the user: a steady accent dot rather than a spinner, and the
9208
+ count in the accent's ink. */
9209
+ ${GROUPED} .tool-group[data-state="waiting"] .tool-group-icon {
9210
+ mask: none;
9211
+ border-radius: 50%;
9212
+ transform: scale(0.6);
9213
+ background: var(--_accent);
9214
+ }
9215
+
9216
+ ${GROUPED} .tool-group[data-state="waiting"] .tool-group-meta {
9217
+ color: var(--_accent-ink);
9218
+ font-weight: 500;
9219
+ }
9220
+
9221
+ ${GROUPED} .tool-group-chevron {
9222
+ flex: none;
9223
+ width: 12px;
9224
+ height: 12px;
9225
+ background: currentColor;
9226
+ mask: var(--_ico-chevron) center / contain no-repeat;
9227
+ opacity: 0.7;
9228
+ transition: transform var(--_motion) var(--_ease);
9229
+ }
9230
+
9231
+ ${GROUPED} .tool-group[data-open="true"] .tool-group-chevron {
9232
+ transform: rotate(90deg);
9233
+ }
9234
+
9235
+ @media (prefers-reduced-motion: reduce) {
9236
+ ${GROUPED} .tool-group[data-state="running"] .tool-group-icon {
9237
+ animation: none;
9238
+ }
9239
+ }
9240
+
9241
+ ${GROUPED} .tool-group-list {
9242
+ display: flex;
9243
+ flex-direction: column;
9244
+ gap: 2px;
9245
+ margin: 2px 0 4px 6px;
9246
+ padding-left: 13px;
9247
+ border-left: 1px solid var(--_border);
9248
+ }
9249
+
9250
+ ${GROUPED} .tool-group[data-open="false"] .tool-group-list {
9251
+ display: none;
9252
+ }
9253
+
9254
+ /* A card inside the group is one line of the list: icon, name, and its
9255
+ Details control at the end, with the arguments and result opening under
9256
+ the whole row. */
9257
+ ${GROUPED} .tool-group .tool-call {
9258
+ max-width: 100%;
9259
+ align-self: stretch;
9260
+ flex-flow: row wrap;
9261
+ align-items: center;
9262
+ gap: 2px 10px;
9263
+ padding: 3px 0;
9264
+ border: 0;
9265
+ border-radius: 0;
9266
+ background: transparent;
9267
+ color: var(--_ink-2);
9268
+ font-family: inherit;
9269
+ font-size: var(--_text-sm);
9270
+ }
9271
+
9272
+ ${GROUPED} .tool-group .tool-call-head {
9273
+ flex: 1 1 auto;
9274
+ justify-content: flex-start;
9275
+ }
9276
+
9277
+ ${GROUPED} .tool-group .tool-call-name {
9278
+ flex: 0 1 auto;
9279
+ font-weight: 450;
9280
+ }
9281
+
9282
+ /* The repeat ordinal on a step whose label an earlier step has. */
9283
+ ${GROUPED} .tool-call-repeat {
9284
+ color: var(--_muted);
9285
+ font-variant-numeric: tabular-nums;
9286
+ }
9287
+
9288
+ ${GROUPED} .tool-group .tool-call-toggle {
9289
+ font-family: inherit;
9290
+ font-size: var(--_text-xs);
9291
+ color: var(--_muted);
9292
+ }
9293
+
9294
+ ${GROUPED} .tool-group .tool-call-body,
9295
+ ${GROUPED} .tool-group .tool-call-approval,
9296
+ ${GROUPED} .tool-group .tool-call-subagent {
9297
+ flex-basis: 100%;
9298
+ }
9299
+
9300
+ /* Grouping is decided as each card is built, so a card mode set afterwards
9301
+ finds groups already drawn. It flattens them: the group and its list stop
9302
+ generating boxes, the row and the repeat ordinals go, and every card lays
9303
+ out as a direct child of the answer, exactly as a card built in that mode
9304
+ does. */
9305
+ ${CARDS} .tool-group,
9306
+ ${CARDS} .tool-group-list {
9307
+ display: contents;
9308
+ }
9309
+
9310
+ ${CARDS} .tool-group-head,
9311
+ ${CARDS} .tool-call-repeat {
9312
+ display: none;
9313
+ }
9314
+
9315
+ /* Each of these declares its own display, which beats the user-agent rule for
9316
+ the hidden attribute, so each needs its own. The host prefix is there for
9317
+ weight, not scope: it lifts these over the card-mode rules above that set
9318
+ display, which are heavier than a bare class and attribute. */
9319
+ :host .tool-group[hidden],
9320
+ :host .tool-group-head[hidden],
9321
+ :host .tool-group-list[hidden] {
9322
+ display: none;
9323
+ }
9324
+ `;
9325
+
9078
9326
  // src/ui/progress/tool_step_styles.ts
9079
9327
  var TOOL_STEP_STYLES = `
9080
9328
  .tool-call {
@@ -9271,8 +9519,6 @@ var TOOL_STEP_STYLES = `
9271
9519
  padding: 6px 8px;
9272
9520
  max-height: 160px;
9273
9521
  overflow: auto;
9274
- /* Scrolling past the end of this must not scroll the page behind it. */
9275
- overscroll-behavior: contain;
9276
9522
  background: var(--_bg);
9277
9523
  border: 1px solid var(--_border);
9278
9524
  border-radius: 6px;
@@ -9718,6 +9964,7 @@ var TOKEN_STYLES = `
9718
9964
  --_line-strong: var(--ag-ui-line-strong, color-mix(in oklch, var(--_fg) 20%, var(--_bg)));
9719
9965
  --_wash: var(--ag-ui-wash, color-mix(in oklch, var(--_fg) 4.5%, var(--_bg)));
9720
9966
  --_accent-ink: var(--ag-ui-accent-ink, color-mix(in oklch, var(--_accent) 82%, var(--_fg)));
9967
+ --_ring: var(--ag-ui-ring, color-mix(in oklch, var(--_accent) 55%, transparent));
9721
9968
  /* The user's turn is the accent at a whisper. With Quiet's graphite accent
9722
9969
  that is a neutral wash; once a host sets a brand accent it becomes a tint
9723
9970
  of the brand, which is the one thing Soft does that Quiet keeps. */
@@ -10049,6 +10296,7 @@ var TOKEN_STYLES = `
10049
10296
  --_ico-check: var(--ag-ui-ico-check, url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 8.5l3 3 6-7'/%3E%3C/svg%3E"));
10050
10297
  --_ico-error: var(--ag-ui-ico-error, url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round'%3E%3Ccircle cx='8' cy='8' r='6.2'/%3E%3Cpath d='M8 4.9v3.7M8 11.1v.1'/%3E%3C/svg%3E"));
10051
10298
  --_ico-declined: var(--ag-ui-ico-declined, url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round'%3E%3Ccircle cx='8' cy='8' r='6.2'/%3E%3Cpath d='M3.8 12.2l8.4-8.4'/%3E%3C/svg%3E"));
10299
+ --_ico-chevron: var(--ag-ui-ico-chevron, url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 3.5l4.5 4.5L6 12.5'/%3E%3C/svg%3E"));
10052
10300
  }
10053
10301
 
10054
10302
  /* The colour drawn on the accent, from the accent's own luminance: white on
@@ -10267,6 +10515,21 @@ var LEDGER_STYLES = `
10267
10515
  letter-spacing: 0.01em;
10268
10516
  }
10269
10517
 
10518
+ /* The step group's row reads as a ledger heading. Unscoped to grouped mode:
10519
+ a card mode hides the row and drops the list's box, so neither reaches a
10520
+ flattened card. */
10521
+ :host([theme~="ledger"]) .tool-group-head {
10522
+ font-family: var(--_code-font);
10523
+ font-size: var(--_text-xs);
10524
+ border-radius: 2px;
10525
+ text-transform: uppercase;
10526
+ letter-spacing: 0.06em;
10527
+ }
10528
+
10529
+ :host([theme~="ledger"]) .tool-group-list {
10530
+ border-left: 1px dashed var(--_line-strong);
10531
+ }
10532
+
10270
10533
  :host([theme~="ledger"]) .chart-block {
10271
10534
  border-top: 1px solid var(--_line-strong);
10272
10535
  border-bottom: 1px solid var(--_line);
@@ -10650,6 +10913,26 @@ var SOFT_STYLES = `
10650
10913
  color: var(--_accent-ink);
10651
10914
  }
10652
10915
 
10916
+ /* The step group's row as a tinted pill. Unscoped to grouped mode: a card
10917
+ mode hides the row and drops the list's box, so neither reaches a
10918
+ flattened card. */
10919
+ :host([theme~="soft"]) .tool-group-head {
10920
+ background: var(--_tint);
10921
+ border: 1px solid var(--_border);
10922
+ border-radius: 999px;
10923
+ padding: 5px 12px 5px 9px;
10924
+ margin-left: 0;
10925
+ }
10926
+
10927
+ :host([theme~="soft"]) .tool-group-head:hover {
10928
+ background: var(--_tint-2);
10929
+ }
10930
+
10931
+ :host([theme~="soft"]) .tool-group-list {
10932
+ border-left-color: var(--_tint-2);
10933
+ margin-left: 14px;
10934
+ }
10935
+
10653
10936
  :host([theme~="soft"]) .chart-block {
10654
10937
  background: var(--_bg);
10655
10938
  border: 1px solid var(--_border);
@@ -11318,8 +11601,6 @@ var MESSAGE_STYLES = `
11318
11601
  .message--assistant pre {
11319
11602
  padding: 8px 10px;
11320
11603
  overflow: auto;
11321
- /* Scrolling past the end of this must not scroll the page behind it. */
11322
- overscroll-behavior: contain;
11323
11604
  background: var(--_bg);
11324
11605
  border: 1px solid var(--_border);
11325
11606
  border-radius: 6px;
@@ -11627,6 +11908,10 @@ var STYLES = [
11627
11908
  QUESTION_STYLES,
11628
11909
  SKILL_STYLES,
11629
11910
  HISTORY_STYLES,
11911
+ // The step group restates, inside a group, card rules the modules above set
11912
+ // at rest; it outweighs them rather than relying on order, and sits ahead
11913
+ // of the themes so a theme restating a group rule at equal weight wins.
11914
+ TOOL_GROUP_STYLES,
11630
11915
  // Each theme's structure comes last: it restates rules the modules above
11631
11916
  // set at rest, at equal weight. Classic and code have no structural
11632
11917
  // module; they are the base stylesheet, and classic adds only its
@@ -15854,6 +16139,200 @@ var ThoughtsBlock = class {
15854
16139
  }
15855
16140
  };
15856
16141
 
16142
+ // src/ui/progress/tool_step_group.ts
16143
+ var OPEN_STATUSES = /* @__PURE__ */ new Set([
16144
+ TOOL_CALL_STATUS.PENDING,
16145
+ TOOL_CALL_STATUS.DEFERRED
16146
+ ]);
16147
+ var RUNNING_SUBAGENT = `.tool-call-subagent > .subagent:is(${[
16148
+ SUBAGENT_PHASE.STARTED,
16149
+ SUBAGENT_PHASE.TOOL_CALL,
16150
+ SUBAGENT_PHASE.TOOL_RESULT
16151
+ ].map((phase) => `[data-phase="${phase}"]`).join(", ")})`;
16152
+ var WATCHED_ATTRIBUTES = [
16153
+ "data-status",
16154
+ "data-asking",
16155
+ "data-phase",
16156
+ "data-agent"
16157
+ ];
16158
+ var ToolStepGroup = class {
16159
+ element;
16160
+ #strings;
16161
+ #list;
16162
+ #head;
16163
+ #label;
16164
+ #meta;
16165
+ /** The steps, in order. Cards only ever join a group, so this is its list's children. */
16166
+ #steps = [];
16167
+ /** Whether the group has already opened itself for the current wait. */
16168
+ #openedForWait = false;
16169
+ constructor(strings) {
16170
+ this.#strings = strings;
16171
+ this.element = document.createElement("div");
16172
+ this.element.className = "tool-group";
16173
+ this.element.setAttribute("part", "tool-group");
16174
+ this.element.setAttribute("data-open", "false");
16175
+ this.#head = document.createElement("button");
16176
+ this.#head.type = "button";
16177
+ this.#head.className = "tool-group-head";
16178
+ this.#head.setAttribute("part", "tool-group-head");
16179
+ this.#head.setAttribute("aria-expanded", "false");
16180
+ const icon = document.createElement("span");
16181
+ icon.className = "tool-group-icon";
16182
+ icon.setAttribute("part", "tool-group-icon");
16183
+ icon.setAttribute("aria-hidden", "true");
16184
+ this.#label = document.createElement("span");
16185
+ this.#label.className = "tool-group-label";
16186
+ this.#label.setAttribute("part", "tool-group-label");
16187
+ this.#meta = document.createElement("span");
16188
+ this.#meta.className = "tool-group-meta";
16189
+ this.#meta.setAttribute("part", "tool-group-meta");
16190
+ const chevron = document.createElement("span");
16191
+ chevron.className = "tool-group-chevron";
16192
+ chevron.setAttribute("aria-hidden", "true");
16193
+ this.#head.append(icon, this.#label, this.#meta, chevron);
16194
+ this.#head.addEventListener("click", () => {
16195
+ this.setOpen(this.element.getAttribute("data-open") !== "true");
16196
+ });
16197
+ this.#list = document.createElement("div");
16198
+ this.#list.className = "tool-group-list";
16199
+ this.#list.setAttribute("part", "tool-group-list");
16200
+ this.element.append(this.#head, this.#list);
16201
+ new MutationObserver(() => {
16202
+ this.#refresh();
16203
+ }).observe(this.#list, {
16204
+ attributes: true,
16205
+ attributeFilter: [...WATCHED_ATTRIBUTES],
16206
+ subtree: true
16207
+ });
16208
+ }
16209
+ /**
16210
+ * Add a card's element as the group's next step, under the label the card
16211
+ * was built with.
16212
+ *
16213
+ * A step whose label an earlier step already has gets a repeat ordinal,
16214
+ * "Fill field (2)", in an element of its own beside the card's name. The
16215
+ * name itself is never rewritten, so a group flattened back into per-call
16216
+ * cards by a card display mode shows exactly the label the card was built
16217
+ * with, and the CSS hides the ordinal there.
16218
+ */
16219
+ add(card, name) {
16220
+ const seen = this.#steps.filter((step) => step.name === name).length + 1;
16221
+ let label = name;
16222
+ if (seen > 1) {
16223
+ const repeat = document.createElement("span");
16224
+ repeat.className = "tool-call-repeat";
16225
+ repeat.setAttribute("part", "tool-card-repeat");
16226
+ repeat.textContent = fillUiString(this.#strings.toolStepRepeat, { n: seen });
16227
+ card.querySelector(".tool-call-name")?.after(repeat);
16228
+ label = `${name} ${repeat.textContent}`;
16229
+ }
16230
+ this.#steps.push({ element: card, name, label });
16231
+ this.#list.append(card);
16232
+ this.#refresh();
16233
+ }
16234
+ /** Show or hide the per-step cards. */
16235
+ setOpen(open) {
16236
+ this.element.setAttribute("data-open", String(open));
16237
+ this.#head.setAttribute("aria-expanded", String(open));
16238
+ }
16239
+ /**
16240
+ * Open the group now, for the wait that is about to start.
16241
+ *
16242
+ * The group opens itself from its mutation observer, which runs a tick after
16243
+ * the card is marked deferred. The approval prompt is drawn, and takes focus,
16244
+ * in the same tick as the mark, while the list is still out of the layout: the
16245
+ * focus is dropped and the scroll it would cause never happens. So the caller
16246
+ * that is about to draw the prompt opens the group first, and this marks the
16247
+ * wait as already answered for, so the observer's later refresh does not open
16248
+ * it again over a user who folded it.
16249
+ */
16250
+ reveal() {
16251
+ this.#openedForWait = true;
16252
+ this.setOpen(true);
16253
+ }
16254
+ /**
16255
+ * The row's note for a running sub-agent, naming it, or `null` when no step
16256
+ * is delegating right now. Only a step that is itself still running counts.
16257
+ */
16258
+ #delegation() {
16259
+ for (const { element } of this.#steps) {
16260
+ const panel = element.getAttribute("data-status") === TOOL_CALL_STATUS.PENDING ? element.querySelector(RUNNING_SUBAGENT) : null;
16261
+ if (panel !== null) {
16262
+ const agent = panel.getAttribute("data-agent");
16263
+ return agent === null ? this.#strings.subAgentWorking : fillUiString(this.#strings.subAgentDelegatedTo, { agent });
16264
+ }
16265
+ }
16266
+ return null;
16267
+ }
16268
+ #refresh() {
16269
+ const strings = this.#strings;
16270
+ const steps = this.#steps;
16271
+ const statuses = steps.map((step) => step.element.getAttribute("data-status") ?? "");
16272
+ const open = steps.filter((_2, i) => OPEN_STATUSES.has(statuses[i]));
16273
+ const count = (status) => statuses.filter((s) => s === status).length;
16274
+ const failed = count(TOOL_CALL_STATUS.ERROR);
16275
+ const declined = count(TOOL_CALL_STATUS.DECLINED);
16276
+ const deferred = count(TOOL_CALL_STATUS.DEFERRED);
16277
+ const asking = steps.filter((step) => step.element.hasAttribute("data-asking")).length;
16278
+ const waiting = deferred + asking;
16279
+ const total = steps.length;
16280
+ if (deferred > 0 && !this.#openedForWait) {
16281
+ this.#openedForWait = true;
16282
+ this.setOpen(true);
16283
+ } else if (deferred === 0) {
16284
+ this.#openedForWait = false;
16285
+ }
16286
+ let state = "done";
16287
+ if (waiting > 0) {
16288
+ state = "waiting";
16289
+ } else if (open.length > 0) {
16290
+ state = "running";
16291
+ } else if (failed > 0) {
16292
+ state = "error";
16293
+ }
16294
+ this.element.setAttribute("data-state", state);
16295
+ this.element.setAttribute("data-count", String(total));
16296
+ const only = total === 1 ? steps[0] : void 0;
16297
+ if (only !== void 0) {
16298
+ this.#label.textContent = only.label;
16299
+ } else {
16300
+ const template = state === "waiting" ? strings.toolGroupSteps : state === "running" ? strings.toolGroupWorking : strings.toolGroupWorked;
16301
+ this.#label.textContent = fillUiString(template, { count: total });
16302
+ }
16303
+ const notes = [];
16304
+ const delegation = this.#delegation();
16305
+ const current = open[0];
16306
+ if (waiting > 0) {
16307
+ notes.push(fillUiString(strings.toolGroupWaiting, { count: waiting }));
16308
+ } else if (delegation !== null) {
16309
+ notes.push(delegation);
16310
+ } else if (current !== void 0 && total > 1) {
16311
+ notes.push(current.label);
16312
+ }
16313
+ if (failed > 0) {
16314
+ notes.push(fillUiString(strings.toolGroupFailed, { count: failed }));
16315
+ }
16316
+ if (declined > 0) {
16317
+ notes.push(fillUiString(strings.toolGroupDeclined, { count: declined }));
16318
+ }
16319
+ this.#meta.replaceChildren(
16320
+ ...notes.map((text4) => {
16321
+ const note = document.createElement("span");
16322
+ note.className = "tool-group-note";
16323
+ note.textContent = text4;
16324
+ return note;
16325
+ })
16326
+ );
16327
+ this.#meta.hidden = notes.length === 0;
16328
+ }
16329
+ };
16330
+
16331
+ // src/ui/transcript/groups_tool_steps.ts
16332
+ function groupsToolSteps(mode) {
16333
+ return mode === TOOL_DISPLAY.GROUPED;
16334
+ }
16335
+
15857
16336
  // src/ui/transcript/render_or_warn.ts
15858
16337
  function renderOrWarn(render, subject) {
15859
16338
  try {
@@ -15960,6 +16439,12 @@ var Transcript = class {
15960
16439
  #toolCards = /* @__PURE__ */ new Map();
15961
16440
  /** Card elements by call id, so a rendering handler can find its own card. */
15962
16441
  #cardElements = /* @__PURE__ */ new Map();
16442
+ /**
16443
+ * Cards whose call is being run, by call id. A card leaves the awaiting ones
16444
+ * the moment its call is dispatched, and a handler that opens a prompt on its
16445
+ * own card is still running long after, so it is found here.
16446
+ */
16447
+ #running = /* @__PURE__ */ new Map();
15963
16448
  /**
15964
16449
  * Call ids whose card was already settled from a streamed server-side result
15965
16450
  * (`TOOL_CALL_RESULT`), so the post-run executeTool sweep doesn't overwrite
@@ -15980,6 +16465,24 @@ var Transcript = class {
15980
16465
  // the answer group while a reasoning model thinks and collapsed once the
15981
16466
  // answer's first text token arrives. `null` outside a reasoning turn.
15982
16467
  #thoughts = null;
16468
+ /** The step group behind each group element, so a new card can find its run. */
16469
+ #stepGroups = /* @__PURE__ */ new WeakMap();
16470
+ /**
16471
+ * An empty comment after each step group that has rendered output, which
16472
+ * every later output is placed before, so a group's outputs come out in call
16473
+ * order. Every card of a round joins its group before any handler runs, so
16474
+ * placing each output straight after the group would put the newest first;
16475
+ * and a marker rather than the last output, because an output may be a
16476
+ * fragment, which leaves nothing behind to place the next one after.
16477
+ */
16478
+ #groupEnds = /* @__PURE__ */ new WeakMap();
16479
+ /**
16480
+ * The step group element each placed output belongs to. A tool's own output
16481
+ * does not end its group: the next call joins the group above it. Without
16482
+ * that, replay -- which draws each restored call's output before the next
16483
+ * call's card -- would split a run the live stream drew as one group.
16484
+ */
16485
+ #outputGroups = /* @__PURE__ */ new WeakMap();
15983
16486
  constructor(host) {
15984
16487
  this.#host = host;
15985
16488
  }
@@ -16075,6 +16578,18 @@ var Transcript = class {
16075
16578
  forgetCards() {
16076
16579
  this.#serverSettled.clear();
16077
16580
  this.#cardElements.clear();
16581
+ this.#running.clear();
16582
+ }
16583
+ /** Remember the card of a call being run, for {@link setAsking}. */
16584
+ holdRunning(callId, card) {
16585
+ this.#running.set(callId, card);
16586
+ }
16587
+ /**
16588
+ * Say whether a prompt is open on the card of a running call. A call id of
16589
+ * none, or one no card was held for, has no card to say it on.
16590
+ */
16591
+ setAsking(callId, asking) {
16592
+ this.#running.get(callId)?.setAsking(asking);
16078
16593
  }
16079
16594
  /** Empty the list back to its empty-state region. The last half of wiping it. */
16080
16595
  empty() {
@@ -16248,7 +16763,23 @@ var Transcript = class {
16248
16763
  if (node === null) {
16249
16764
  return;
16250
16765
  }
16251
- this.#cardElements.get(call.id)?.after(node);
16766
+ const anchor = this.#cardElements.get(call.id);
16767
+ const group = anchor?.closest(".tool-group");
16768
+ if (group === null || group === void 0) {
16769
+ anchor?.after(node);
16770
+ } else {
16771
+ let end = this.#groupEnds.get(group);
16772
+ if (end === void 0) {
16773
+ end = document.createComment("");
16774
+ group.after(end);
16775
+ this.#groupEnds.set(group, end);
16776
+ }
16777
+ const placed = node instanceof DocumentFragment ? [...node.children] : [node];
16778
+ end.before(node);
16779
+ for (const output of placed) {
16780
+ this.#outputGroups.set(output, group);
16781
+ }
16782
+ }
16252
16783
  this.afterGrew();
16253
16784
  }
16254
16785
  /** Update the empty state and follow the foot, after the transcript grew. */
@@ -16276,11 +16807,49 @@ var Transcript = class {
16276
16807
  formatPayload: (payload) => this.#host.formatToolPayload(payload)
16277
16808
  });
16278
16809
  this.#toolCards.set(call.id, card);
16279
- this.ensureGroup().appendChild(card.element);
16810
+ if (groupsToolSteps(this.#host.toolDisplay())) {
16811
+ this.#stepGroup().add(card.element, summary);
16812
+ } else {
16813
+ this.ensureGroup().appendChild(card.element);
16814
+ }
16280
16815
  this.updateEmptyState();
16281
16816
  this.follow();
16282
16817
  return card;
16283
16818
  }
16819
+ /**
16820
+ * Open the step group ``card`` sits in, at once, ahead of a prompt about to be
16821
+ * drawn inside it. A no-op for a card drawn in a card display mode, which has
16822
+ * no group.
16823
+ */
16824
+ revealStep(card) {
16825
+ const group = card.element.closest(".tool-group");
16826
+ (group === null ? void 0 : this.#stepGroups.get(group))?.reveal();
16827
+ }
16828
+ /**
16829
+ * The step group a new card joins. Consecutive cards share one, and so do
16830
+ * cards separated only by the group's own rendered output; anything else in
16831
+ * between (text, a confirmation, a question) starts the next.
16832
+ *
16833
+ * The pending dots are not skipped over: a tool call takes them down before
16834
+ * its card is drawn, so they are never what a card finds at the foot of the
16835
+ * turn.
16836
+ */
16837
+ #stepGroup() {
16838
+ const answer = this.ensureGroup();
16839
+ const last = answer.lastElementChild;
16840
+ const existing = last === null ? void 0 : this.#stepGroups.get(this.#outputGroups.get(last) ?? last);
16841
+ if (existing !== void 0) {
16842
+ return existing;
16843
+ }
16844
+ const group = new ToolStepGroup(this.#host.strings());
16845
+ this.#stepGroups.set(group.element, group);
16846
+ if (last === null) {
16847
+ answer.prepend(group.element);
16848
+ } else {
16849
+ last.after(group.element);
16850
+ }
16851
+ return group;
16852
+ }
16284
16853
  };
16285
16854
 
16286
16855
  // src/core/activity_registry.ts
@@ -18440,6 +19009,7 @@ var AgUiChat = class extends HTMLElement {
18440
19009
  askUser: () => this.askUser,
18441
19010
  askUserRenderer: () => this.askUserRenderer,
18442
19011
  decision: this.#decision,
19012
+ setAsking: (callId, asking) => this.#transcript.setAsking(callId, asking),
18443
19013
  ensureGroup: () => this.#transcript.ensureGroup(),
18444
19014
  strings: () => this.#strings,
18445
19015
  hidePending: () => this.#transcript.hidePending(),
@@ -18492,6 +19062,7 @@ var AgUiChat = class extends HTMLElement {
18492
19062
  resolveTool: (name) => this.#tools.resolve(name),
18493
19063
  toolSummaries: () => this.toolSummaries,
18494
19064
  serverSummary: (name) => this.#tools.summary(name),
19065
+ toolDisplay: () => this.toolDisplay,
18495
19066
  // A thunk over the live property, not the property itself: the card keeps
18496
19067
  // this for the life of the call, and the result region is filled when the
18497
19068
  // tool settles -- which can be long after a host set the hook.
@@ -18963,18 +19534,23 @@ var AgUiChat = class extends HTMLElement {
18963
19534
  }
18964
19535
  /**
18965
19536
  * How much detail tool-call cards show, from the `data-tool-display`
18966
- * attribute (`minimal` / `inline` / `compact` / `full`). Defaults to `full`.
19537
+ * attribute (`grouped` / `minimal` / `inline` / `compact` / `full`).
19538
+ * Defaults to `grouped`, for an absent attribute and for a value the
19539
+ * element does not know; `full` is the per-call card default before
19540
+ * `grouped` existed.
18967
19541
  *
18968
- * Applied by the shadow CSS from the attribute itself, so changing it
18969
- * restyles every card already in the transcript rather than only the ones
18970
- * built afterwards.
19542
+ * The four card modes are applied by the shadow CSS from the attribute
19543
+ * itself, so changing between them restyles every card already in the
19544
+ * transcript. Whether calls fold into a step group is decided as each card
19545
+ * is built: switching to `grouped` folds only the calls drawn afterwards,
19546
+ * and switching away flattens the existing groups, again by CSS.
18971
19547
  */
18972
19548
  get toolDisplay() {
18973
19549
  const attr = this.getAttribute("data-tool-display");
18974
- if (attr === TOOL_DISPLAY.INLINE || attr === TOOL_DISPLAY.MINIMAL || attr === TOOL_DISPLAY.COMPACT) {
19550
+ if (attr === TOOL_DISPLAY.INLINE || attr === TOOL_DISPLAY.MINIMAL || attr === TOOL_DISPLAY.COMPACT || attr === TOOL_DISPLAY.FULL) {
18975
19551
  return attr;
18976
19552
  }
18977
- return TOOL_DISPLAY.FULL;
19553
+ return TOOL_DISPLAY.GROUPED;
18978
19554
  }
18979
19555
  set toolDisplay(value) {
18980
19556
  this.setAttribute("data-tool-display", value);
@@ -20342,7 +20918,7 @@ function setControlValue(el2, value) {
20342
20918
  }
20343
20919
 
20344
20920
  // src/version.ts
20345
- var VERSION = "0.45.0";
20921
+ var VERSION = "0.46.0";
20346
20922
  export {
20347
20923
  ATTACHMENT_EVENT,
20348
20924
  AgUiChat,