@artooi/ag-ui-web-component 0.45.0 → 0.47.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 (91) hide show
  1. package/CHANGELOG.md +146 -1
  2. package/README.md +191 -25
  3. package/dist/ag-ui-web-component.bundle.js +399 -157
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +14 -1
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +47 -4
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/core/chat_registry.d.ts +23 -0
  10. package/dist/core/chat_registry.d.ts.map +1 -0
  11. package/dist/core/events/run_finished_detail.d.ts +6 -0
  12. package/dist/core/events/run_finished_detail.d.ts.map +1 -1
  13. package/dist/core/events/thread_detail.d.ts +27 -0
  14. package/dist/core/events/thread_detail.d.ts.map +1 -0
  15. package/dist/core/run_handlers.d.ts +6 -1
  16. package/dist/core/run_handlers.d.ts.map +1 -1
  17. package/dist/dom/animations.d.ts +21 -6
  18. package/dist/dom/animations.d.ts.map +1 -1
  19. package/dist/dom/highlight_overlay.d.ts +7 -1
  20. package/dist/dom/highlight_overlay.d.ts.map +1 -1
  21. package/dist/dom/ring_color.d.ts +14 -0
  22. package/dist/dom/ring_color.d.ts.map +1 -0
  23. package/dist/index.d.ts +3 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +857 -86
  26. package/dist/index.js.map +4 -4
  27. package/dist/tools/tool_catalog.d.ts +5 -0
  28. package/dist/tools/tool_catalog.d.ts.map +1 -1
  29. package/dist/tools/tool_dispatch.d.ts.map +1 -1
  30. package/dist/ui/history/conversation_history.d.ts +10 -1
  31. package/dist/ui/history/conversation_history.d.ts.map +1 -1
  32. package/dist/ui/interrupts/confirmation_styles.d.ts.map +1 -1
  33. package/dist/ui/placement/panel_placement.d.ts +6 -0
  34. package/dist/ui/placement/panel_placement.d.ts.map +1 -1
  35. package/dist/ui/placement/small_viewport_styles.d.ts.map +1 -1
  36. package/dist/ui/placement/surface_for.d.ts +12 -1
  37. package/dist/ui/placement/surface_for.d.ts.map +1 -1
  38. package/dist/ui/progress/thoughts_styles.d.ts.map +1 -1
  39. package/dist/ui/progress/tool_call_card.d.ts +8 -0
  40. package/dist/ui/progress/tool_call_card.d.ts.map +1 -1
  41. package/dist/ui/progress/tool_group_styles.d.ts +2 -0
  42. package/dist/ui/progress/tool_group_styles.d.ts.map +1 -0
  43. package/dist/ui/progress/tool_step_group.d.ts +47 -0
  44. package/dist/ui/progress/tool_step_group.d.ts.map +1 -0
  45. package/dist/ui/progress/tool_step_styles.d.ts.map +1 -1
  46. package/dist/ui/shell/token_styles.d.ts.map +1 -1
  47. package/dist/ui/styles.d.ts.map +1 -1
  48. package/dist/ui/themes/ledger_styles.d.ts.map +1 -1
  49. package/dist/ui/themes/soft_styles.d.ts.map +1 -1
  50. package/dist/ui/transcript/groups_tool_steps.d.ts +13 -0
  51. package/dist/ui/transcript/groups_tool_steps.d.ts.map +1 -0
  52. package/dist/ui/transcript/message_styles.d.ts.map +1 -1
  53. package/dist/ui/transcript/transcript.d.ts +16 -1
  54. package/dist/ui/transcript/transcript.d.ts.map +1 -1
  55. package/dist/ui/ui_strings.d.ts +18 -0
  56. package/dist/ui/ui_strings.d.ts.map +1 -1
  57. package/package.json +1 -1
  58. package/src/constants.ts +15 -1
  59. package/src/core/ag_ui_chat.ts +122 -11
  60. package/src/core/chat_registry.ts +53 -0
  61. package/src/core/events/run_finished_detail.ts +6 -0
  62. package/src/core/events/thread_detail.ts +26 -0
  63. package/src/core/run_handlers.ts +9 -4
  64. package/src/dom/animations.ts +30 -31
  65. package/src/dom/highlight_overlay.ts +9 -5
  66. package/src/dom/ring_color.ts +85 -0
  67. package/src/index.ts +3 -0
  68. package/src/tools/tool_catalog.ts +23 -10
  69. package/src/tools/tool_dispatch.ts +26 -11
  70. package/src/ui/history/conversation_history.ts +28 -4
  71. package/src/ui/interrupts/confirmation_styles.ts +0 -2
  72. package/src/ui/placement/embedded_styles.ts +2 -2
  73. package/src/ui/placement/panel_placement.ts +34 -9
  74. package/src/ui/placement/preset_styles.ts +1 -1
  75. package/src/ui/placement/sidebar_styles.ts +1 -1
  76. package/src/ui/placement/small_viewport_styles.ts +5 -0
  77. package/src/ui/placement/surface_for.ts +12 -1
  78. package/src/ui/progress/thoughts_styles.ts +0 -2
  79. package/src/ui/progress/tool_call_card.ts +29 -8
  80. package/src/ui/progress/tool_group_styles.ts +232 -0
  81. package/src/ui/progress/tool_step_group.ts +272 -0
  82. package/src/ui/progress/tool_step_styles.ts +0 -2
  83. package/src/ui/shell/token_styles.ts +4 -2
  84. package/src/ui/styles.ts +5 -0
  85. package/src/ui/themes/ledger_styles.ts +15 -0
  86. package/src/ui/themes/soft_styles.ts +20 -0
  87. package/src/ui/transcript/groups_tool_steps.ts +16 -0
  88. package/src/ui/transcript/message_styles.ts +0 -2
  89. package/src/ui/transcript/transcript.ts +109 -3
  90. package/src/ui/ui_strings.ts +31 -0
  91. package/src/version.ts +1 -1
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ var UNREAD_EVENT = "ag-ui-unread";
6
6
  var STATE_EVENT = "ag-ui-state";
7
7
  var ATTACHMENT_EVENT = "ag-ui-attachments";
8
8
  var RUN_FINISHED_EVENT = "ag-ui-run-finished";
9
+ var THREAD_EVENT = "ag-ui-thread";
9
10
  var CUSTOM_AGENT_EVENT = "ag-ui-custom";
10
11
  var INVALIDATE_EVENT = "ag-ui-invalidate";
11
12
  var FEEDBACK_EVENT = "ag-ui-feedback";
@@ -55,6 +56,7 @@ var ATTACHMENT_STATUS = {
55
56
  };
56
57
  var DEFAULT_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
57
58
  var TOOL_DISPLAY = {
59
+ GROUPED: "grouped",
58
60
  INLINE: "inline",
59
61
  MINIMAL: "minimal",
60
62
  COMPACT: "compact",
@@ -353,8 +355,79 @@ var ClientToolRegistry = class {
353
355
  }
354
356
  };
355
357
 
358
+ // src/core/chat_registry.ts
359
+ var CONNECTED_CHATS = /* @__PURE__ */ new Set();
360
+ var ChatRegistration = class {
361
+ #chat;
362
+ constructor(chat) {
363
+ this.#chat = chat;
364
+ }
365
+ /**
366
+ * Join, as the most recently connected chat. An element moved in the DOM
367
+ * left on disconnect before it reconnected, so it rejoins at the end and no
368
+ * entry needs removing first.
369
+ */
370
+ join() {
371
+ CONNECTED_CHATS.add(this.#chat);
372
+ }
373
+ /** Leave. The previous chat, if one is still connected, becomes the latest. */
374
+ leave() {
375
+ CONNECTED_CHATS.delete(this.#chat);
376
+ }
377
+ /** The most recently connected chat still in the document, or `null`. */
378
+ static latest() {
379
+ let latest = null;
380
+ for (const chat of CONNECTED_CHATS) {
381
+ latest = chat;
382
+ }
383
+ return latest;
384
+ }
385
+ };
386
+
387
+ // src/dom/ring_color.ts
388
+ var RING_TOKEN = "--ag-ui-highlight-ring-color";
389
+ var ACCENT_TOKEN = "--ag-ui-accent";
390
+ var CHAT_ACCENT = "--_accent";
391
+ var PACKAGE_ACCENT = "#4f46e5";
392
+ function ringColor(target, chat) {
393
+ const onTarget = readToken(target, RING_TOKEN);
394
+ if (onTarget !== "") {
395
+ return onTarget;
396
+ }
397
+ const owner = chat ?? ChatRegistration.latest();
398
+ const onChat = owner === null ? "" : resolveOn(owner, RING_TOKEN);
399
+ if (onChat !== "") {
400
+ return onChat;
401
+ }
402
+ const accent = readToken(target, ACCENT_TOKEN);
403
+ if (accent !== "") {
404
+ return accent;
405
+ }
406
+ const chatAccent = owner === null ? "" : resolveOn(owner, CHAT_ACCENT);
407
+ return chatAccent === "" ? PACKAGE_ACCENT : chatAccent;
408
+ }
409
+ function readToken(el2, name) {
410
+ return window.getComputedStyle(el2).getPropertyValue(name).trim();
411
+ }
412
+ function resolveOn(chat, name) {
413
+ const value = readToken(chat, name);
414
+ const root = chat.shadowRoot;
415
+ if (value === "" || root === null) {
416
+ return value;
417
+ }
418
+ const probe = document.createElement("span");
419
+ probe.hidden = true;
420
+ probe.style.color = value;
421
+ if (probe.style.color === "") {
422
+ return "";
423
+ }
424
+ root.appendChild(probe);
425
+ const resolved = window.getComputedStyle(probe).color;
426
+ probe.remove();
427
+ return resolved;
428
+ }
429
+
356
430
  // src/dom/highlight_overlay.ts
357
- var ACCENT = "#4f46e5";
358
431
  function tokenOn(target, name, fallback) {
359
432
  const value = window.getComputedStyle(target).getPropertyValue(name).trim();
360
433
  return value === "" ? fallback : value;
@@ -439,7 +512,7 @@ function safeColor(value) {
439
512
  return CSS.supports("color", value) ? value : null;
440
513
  }
441
514
  function ringPaint(target, options) {
442
- const color = safeColor(options.color) ?? tokenOn(target, "--ag-ui-accent", ACCENT);
515
+ const color = safeColor(options.color) ?? ringColor(target, options.chat);
443
516
  if (options.gradient !== true) {
444
517
  return `border-color: ${color}`;
445
518
  }
@@ -503,9 +576,6 @@ function setNativeChecked(el2, checked) {
503
576
  }
504
577
 
505
578
  // src/dom/animations.ts
506
- var ACCENT2 = "#4f46e5";
507
- var ACCENT_HALO = "rgba(79, 70, 229, 0.4)";
508
- var ACCENT_PROPERTY = "--ag-ui-accent";
509
579
  function delay(ms) {
510
580
  return new Promise((resolve) => {
511
581
  setTimeout(resolve, ms);
@@ -520,12 +590,8 @@ function motionDelay(ms) {
520
590
  }
521
591
  return delay(ms);
522
592
  }
523
- function accentColor(el2, fallback) {
524
- const themed = window.getComputedStyle(el2).getPropertyValue(ACCENT_PROPERTY).trim();
525
- return themed === "" ? fallback : themed;
526
- }
527
- function ringShadow(el2) {
528
- return `0 0 0 3px ${accentColor(el2, ACCENT_HALO)}`;
593
+ function ringShadow(el2, chat) {
594
+ return `0 0 0 3px color-mix(in srgb, ${ringColor(el2, chat)} 40%, transparent)`;
529
595
  }
530
596
  async function typeInto(el2, value, options = {}) {
531
597
  const charDelayMs = options.charDelayMs ?? 35;
@@ -544,7 +610,7 @@ async function highlightThenClick(el2, options = {}) {
544
610
  const highlightMs = options.highlightMs ?? 280;
545
611
  const previousOutline = el2.style.outline;
546
612
  const previousOffset = el2.style.outlineOffset;
547
- el2.style.outline = `2px solid ${accentColor(el2, ACCENT2)}`;
613
+ el2.style.outline = `2px solid ${ringColor(el2, options.chat)}`;
548
614
  el2.style.outlineOffset = "2px";
549
615
  await delay(highlightMs);
550
616
  el2.style.outline = previousOutline;
@@ -596,7 +662,8 @@ async function flashRing(el2, options, focusByDefault) {
596
662
  scrim: options.scrim === true,
597
663
  gradient: options.gradient === true,
598
664
  ...options.color === void 0 ? {} : { color: options.color },
599
- ...options.ringPadding === void 0 ? {} : { padding: options.ringPadding }
665
+ ...options.ringPadding === void 0 ? {} : { padding: options.ringPadding },
666
+ ...options.chat === void 0 ? {} : { chat: options.chat }
600
667
  });
601
668
  await delay(flashMs);
602
669
  dismiss();
@@ -605,7 +672,7 @@ async function flashRing(el2, options, focusByDefault) {
605
672
  const previousOutline = el2.style.outline;
606
673
  const previousOffset = el2.style.outlineOffset;
607
674
  const previousTransition = el2.style.transition;
608
- const color = options.color ?? accentColor(el2, ACCENT2);
675
+ const color = options.color ?? ringColor(el2, options.chat);
609
676
  el2.style.outline = `3px solid ${color}`;
610
677
  el2.style.outlineOffset = "2px";
611
678
  const fadeMs = prefersReducedMotion() ? 0 : Math.round(flashMs * FLASH_FADE_RATIO);
@@ -632,7 +699,7 @@ async function pressThenClick(el2, options = {}) {
632
699
  const previousShadow = el2.style.boxShadow;
633
700
  el2.style.transition = "transform 80ms ease";
634
701
  el2.style.transform = "scale(0.96)";
635
- el2.style.boxShadow = ringShadow(el2);
702
+ el2.style.boxShadow = ringShadow(el2, options.chat);
636
703
  await motionDelay(pressMs);
637
704
  el2.style.transform = previousTransform;
638
705
  el2.style.transition = previousTransition;
@@ -655,7 +722,7 @@ async function selectOption(el2, value, options = {}) {
655
722
  const highlightMs = options.highlightMs ?? 220;
656
723
  const previousOutline = el2.style.outline;
657
724
  const previousOffset = el2.style.outlineOffset;
658
- el2.style.outline = `2px solid ${accentColor(el2, ACCENT2)}`;
725
+ el2.style.outline = `2px solid ${ringColor(el2, options.chat)}`;
659
726
  el2.style.outlineOffset = "2px";
660
727
  await motionDelay(highlightMs);
661
728
  setNativeValue(el2, option.value);
@@ -667,7 +734,7 @@ async function selectOption(el2, value, options = {}) {
667
734
  async function toggleControl(el2, checked, options = {}) {
668
735
  const flashMs = options.flashMs ?? 200;
669
736
  const previousShadow = el2.style.boxShadow;
670
- el2.style.boxShadow = ringShadow(el2);
737
+ el2.style.boxShadow = ringShadow(el2, options.chat);
671
738
  await motionDelay(flashMs);
672
739
  setNativeChecked(el2, checked);
673
740
  el2.dispatchEvent(new Event("input", { bubbles: true }));
@@ -1014,6 +1081,13 @@ var DEFAULT_UI_STRINGS = {
1014
1081
  declinedLabel: "Declined",
1015
1082
  interruptedLabel: "Not finished",
1016
1083
  details: "Details",
1084
+ toolGroupSteps: "{count} steps",
1085
+ toolGroupWorking: "Working through {count} steps",
1086
+ toolGroupWorked: "Worked through {count} steps",
1087
+ toolGroupWaiting: "{count} waiting for you",
1088
+ toolGroupFailed: "{count} failed",
1089
+ toolGroupDeclined: "{count} declined",
1090
+ toolStepRepeat: "({n})",
1017
1091
  subAgentWorking: "Working\u2026",
1018
1092
  subAgentDelegatedTo: "Delegated to {agent}",
1019
1093
  subAgentFinished: "{agent} finished",
@@ -1447,14 +1521,19 @@ var ToolCatalog = class {
1447
1521
  });
1448
1522
  const renderer = this.#host.askUserRenderer();
1449
1523
  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);
1524
+ this.#host.setAsking(callId, true);
1525
+ try {
1526
+ if (renderer === null) {
1527
+ answer = await builtIn();
1528
+ } else {
1529
+ try {
1530
+ answer = await renderer.call(this.#host.element, request, { signal });
1531
+ } catch (error) {
1532
+ answer = await this.#afterRendererFailed(error, signal, callId, builtIn);
1533
+ }
1457
1534
  }
1535
+ } finally {
1536
+ this.#host.setAsking(callId, false);
1458
1537
  }
1459
1538
  this.#host.decision.close();
1460
1539
  this.#host.updateEmptyState();
@@ -1752,6 +1831,7 @@ var ToolDispatch = class {
1752
1831
  const card = this.#host.transcript.cardFor(call);
1753
1832
  this.#host.transcript.forgetCard(call.id);
1754
1833
  this.#host.transcript.setCardElement(call.id, card.element);
1834
+ this.#host.transcript.holdRunning(call.id, card);
1755
1835
  const tool = this.#host.tools.wasAdvertised(call.name) ? this.#host.tools.resolve(call.name) : null;
1756
1836
  if (tool === null) {
1757
1837
  if (!this.#host.transcript.isServerSettled(call.id)) {
@@ -1784,15 +1864,21 @@ var ToolDispatch = class {
1784
1864
  request.message = confirmText;
1785
1865
  }
1786
1866
  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;
1867
+ card.setAsking(true);
1868
+ let accepted;
1869
+ try {
1870
+ const decision = requestConfirmation(this.#host.transcript.ensureGroup(), request, {
1871
+ signal,
1872
+ strings: this.#host.strings(),
1873
+ // Offered only where it can be honoured -- see `#confirmationRule`.
1874
+ ...rule === "destructive" ? { onAlwaysAllow: () => this.#sessionApproved.add(call.name) } : {}
1875
+ });
1876
+ this.#host.transcript.updateEmptyState();
1877
+ this.#host.transcript.follow();
1878
+ accepted = await decision;
1879
+ } finally {
1880
+ card.setAsking(false);
1881
+ }
1796
1882
  this.#host.decision.close();
1797
1883
  if (this.#host.tenure() !== tenure) {
1798
1884
  return null;
@@ -1893,6 +1979,9 @@ var ToolDispatch = class {
1893
1979
  request.args = card.args;
1894
1980
  }
1895
1981
  card?.mark(TOOL_CALL_STATUS.DEFERRED);
1982
+ if (card !== void 0) {
1983
+ this.#host.transcript.revealStep(card);
1984
+ }
1896
1985
  const builtIn = () => requestApproval(card?.approvalSlot ?? this.#host.transcript.ensureGroup(), request, {
1897
1986
  signal,
1898
1987
  strings: this.#host.strings(),
@@ -4399,7 +4488,7 @@ var ConversationHistory = class {
4399
4488
  this.#host.conversationStore().setActiveThread(threadId);
4400
4489
  this.#threadId = threadId;
4401
4490
  this.#host.setRunning(false);
4402
- await this.rehydrate();
4491
+ await this.rehydrate("switch");
4403
4492
  }
4404
4493
  /** Rename a thread, then reload the list that shows its title. */
4405
4494
  renameThread(threadId, title) {
@@ -4417,6 +4506,7 @@ var ConversationHistory = class {
4417
4506
  this.#host.resetState();
4418
4507
  this.adoptActiveThread();
4419
4508
  this.#host.setRunning(false);
4509
+ this.#host.threadChanged(this.#threadId, "delete");
4420
4510
  }
4421
4511
  void this.refreshDrawer();
4422
4512
  }
@@ -4593,8 +4683,11 @@ var ConversationHistory = class {
4593
4683
  * for the run it would have resumed -- unless what replaced it is a restore
4594
4684
  * of the same conversation, still current, which answers for that run
4595
4685
  * instead; see `#forgetStopped`.
4686
+ *
4687
+ * `reason` is what the thread event this restore ends with reports as why
4688
+ * the conversation changed. A restore that is stopped reports nothing.
4596
4689
  */
4597
- async rehydrate() {
4690
+ async rehydrate(reason) {
4598
4691
  this.#generation += 1;
4599
4692
  const generation = this.#generation;
4600
4693
  const threadId = this.#threadId;
@@ -4644,11 +4737,17 @@ var ConversationHistory = class {
4644
4737
  }
4645
4738
  }
4646
4739
  }
4740
+ if (checkpoint === null) {
4741
+ this.#noticeIfRunUnfinished(messages);
4742
+ }
4743
+ this.#host.threadChanged(threadId, reason);
4647
4744
  if (checkpoint !== null) {
4745
+ if (!this.#restoring(generation)) {
4746
+ this.#forgetStopped(store, threadId, checkpoint);
4747
+ return;
4748
+ }
4648
4749
  await this.#resumeFrom(checkpoint, generation);
4649
- return;
4650
4750
  }
4651
- this.#noticeIfRunUnfinished(messages);
4652
4751
  } finally {
4653
4752
  hold.release();
4654
4753
  if (this.#loading === waiting) {
@@ -5629,6 +5728,8 @@ function surfaceFor(placement) {
5629
5728
 
5630
5729
  // src/ui/placement/panel_placement.ts
5631
5730
  var SIZE_KEY = "ag-ui-chat:size";
5731
+ var DRAG_WIDTH = "--_drag-width";
5732
+ var DRAG_HEIGHT = "--_drag-height";
5632
5733
  var LAUNCHER_KEY = "ag-ui-chat:launcher";
5633
5734
  function asPoint(value) {
5634
5735
  if (typeof value !== "object" || value === null) {
@@ -5712,6 +5813,17 @@ var PanelPlacement = class {
5712
5813
  restoreSize() {
5713
5814
  this.#applySize(this.#readSize());
5714
5815
  }
5816
+ /**
5817
+ * Drop the dragged size and its stored copy, so the placement's own size and
5818
+ * the host's tokens apply again. For a host entering a sized state of its
5819
+ * own, which a user's drag would otherwise outrank.
5820
+ */
5821
+ resetSize() {
5822
+ this.#host.element.style.removeProperty(DRAG_WIDTH);
5823
+ this.#host.element.style.removeProperty(DRAG_HEIGHT);
5824
+ this.#host.clearPreference(SIZE_KEY);
5825
+ this.syncResizeAnchor();
5826
+ }
5715
5827
  /**
5716
5828
  * A zero-sized box carrying the host's viewport insets as padding, so they
5717
5829
  * can be read back as used pixel lengths. See the `.viewport-probe` rule for
@@ -5900,12 +6012,12 @@ var PanelPlacement = class {
5900
6012
  */
5901
6013
  #measureAnchor() {
5902
6014
  const before = this.#host.element.getBoundingClientRect();
5903
- const width = this.#host.element.style.getPropertyValue("--ag-ui-width");
5904
- const height = this.#host.element.style.getPropertyValue("--ag-ui-height");
6015
+ const width = this.#host.element.style.getPropertyValue(DRAG_WIDTH);
6016
+ const height = this.#host.element.style.getPropertyValue(DRAG_HEIGHT);
5905
6017
  const shrunk = this.#probeAnchor(before, -1);
5906
6018
  const grown = shrunk.x === null || shrunk.y === null ? this.#probeAnchor(before, 1) : shrunk;
5907
- this.#restoreProperty("--ag-ui-width", width);
5908
- this.#restoreProperty("--ag-ui-height", height);
6019
+ this.#restoreProperty(DRAG_WIDTH, width);
6020
+ this.#restoreProperty(DRAG_HEIGHT, height);
5909
6021
  return {
5910
6022
  // Neither direction moved it: the axis cannot be resized at all, so the
5911
6023
  // floating default is the best answer available and is the one the
@@ -5951,7 +6063,7 @@ var PanelPlacement = class {
5951
6063
  /**
5952
6064
  * Write a dragged size onto the host, on the axes this placement leaves free.
5953
6065
  *
5954
- * Writing the custom property rather than inline `width` / `height` does not
6066
+ * Writing a custom property rather than inline `width` / `height` does not
5955
6067
  * by itself leave placement in charge: an inline custom property still
5956
6068
  * outranks a `:host([placement=…])` rule setting the same property, so a
5957
6069
  * height dragged while floating would cap a docked sidebar asking for
@@ -5965,10 +6077,10 @@ var PanelPlacement = class {
5965
6077
  return;
5966
6078
  }
5967
6079
  if (size.width !== void 0) {
5968
- this.#host.element.style.setProperty("--ag-ui-width", `${size.width}px`);
6080
+ this.#host.element.style.setProperty(DRAG_WIDTH, `${size.width}px`);
5969
6081
  }
5970
6082
  if (size.height !== void 0 && axis === "both") {
5971
- this.#host.element.style.setProperty("--ag-ui-height", `${size.height}px`);
6083
+ this.#host.element.style.setProperty(DRAG_HEIGHT, `${size.height}px`);
5972
6084
  }
5973
6085
  }
5974
6086
  /**
@@ -5981,10 +6093,10 @@ var PanelPlacement = class {
5981
6093
  releaseOwnedAxes() {
5982
6094
  const axis = this.#resizeAxis();
5983
6095
  if (axis !== "both") {
5984
- this.#host.element.style.removeProperty("--ag-ui-height");
6096
+ this.#host.element.style.removeProperty(DRAG_HEIGHT);
5985
6097
  }
5986
6098
  if (axis === "none") {
5987
- this.#host.element.style.removeProperty("--ag-ui-width");
6099
+ this.#host.element.style.removeProperty(DRAG_WIDTH);
5988
6100
  }
5989
6101
  }
5990
6102
  /**
@@ -6887,9 +6999,7 @@ var ToolCallCard = class {
6887
6999
  this.#formatPayload = options.formatPayload ?? null;
6888
7000
  this.element = document.createElement("div");
6889
7001
  this.element.className = "tool-call";
6890
- this.element.setAttribute("part", "tool-card");
6891
7002
  this.element.setAttribute("data-tool-name", name);
6892
- this.element.setAttribute("data-status", TOOL_CALL_STATUS.PENDING);
6893
7003
  this.element.setAttribute("data-expanded", "false");
6894
7004
  const head = document.createElement("div");
6895
7005
  head.className = "tool-call-head";
@@ -6904,8 +7014,7 @@ var ToolCallCard = class {
6904
7014
  label.textContent = summary ?? name;
6905
7015
  this.#status = document.createElement("span");
6906
7016
  this.#status.className = "tool-call-status";
6907
- this.#status.setAttribute("part", "tool-card-status");
6908
- this.#status.textContent = statusLabels(strings)[TOOL_CALL_STATUS.PENDING];
7017
+ this.#setStatus(TOOL_CALL_STATUS.PENDING);
6909
7018
  this.#decision = document.createElement("span");
6910
7019
  this.#decision.className = "tool-call-decision";
6911
7020
  this.#decision.setAttribute("part", "tool-card-decision");
@@ -6953,8 +7062,17 @@ var ToolCallCard = class {
6953
7062
  if (this.#settled) {
6954
7063
  return;
6955
7064
  }
6956
- this.element.setAttribute("data-status", status);
6957
- this.#status.textContent = statusLabels(this.#strings)[status];
7065
+ this.#setStatus(status);
7066
+ }
7067
+ /**
7068
+ * Say whether a prompt is open on this call, a question it asked or a
7069
+ * confirmation it needs. A flag of its own beside the status, which stays
7070
+ * `pending`: the call is still running as far as the status goes, and a card
7071
+ * display mode that reads the status draws it exactly as before. The step
7072
+ * group reads the flag to tell a call waiting on a person from one working.
7073
+ */
7074
+ setAsking(asking) {
7075
+ this.element.toggleAttribute("data-asking", asking);
6958
7076
  }
6959
7077
  /**
6960
7078
  * Record that a human approved or declined this call, as a line in the card.
@@ -6979,8 +7097,7 @@ var ToolCallCard = class {
6979
7097
  return;
6980
7098
  }
6981
7099
  this.#settled = true;
6982
- this.element.setAttribute("data-status", status);
6983
- this.#status.textContent = statusLabels(this.#strings)[status];
7100
+ this.#setStatus(status);
6984
7101
  this.#resultLabel.textContent = resultLabels(this.#strings)[status];
6985
7102
  this.#renderPayload(
6986
7103
  this.#resultBody,
@@ -6989,6 +7106,20 @@ var ToolCallCard = class {
6989
7106
  );
6990
7107
  this.#resultSection.hidden = false;
6991
7108
  }
7109
+ /**
7110
+ * Write the status everywhere it is read from, in one place so they cannot
7111
+ * disagree: `data-status` for the shadow CSS, the pill's text, and the part
7112
+ * lists. `::part()` takes no attribute selector, so the card and its pill also
7113
+ * name the status as a part (`tool-card-error`, `tool-card-status-error`), the
7114
+ * modifier convention a run notice uses, and a host styles failures alone.
7115
+ * Setting the whole list replaces the previous modifier.
7116
+ */
7117
+ #setStatus(status) {
7118
+ this.element.setAttribute("data-status", status);
7119
+ this.element.setAttribute("part", `tool-card tool-card-${status}`);
7120
+ this.#status.setAttribute("part", `tool-card-status tool-card-status-${status}`);
7121
+ this.#status.textContent = statusLabels(this.#strings)[status];
7122
+ }
6992
7123
  /**
6993
7124
  * Fill one body region, offering it to the host formatter first.
6994
7125
  *
@@ -8038,8 +8169,6 @@ var CONFIRMATION_STYLES = `
8038
8169
  padding: 8px 10px;
8039
8170
  max-height: 140px;
8040
8171
  overflow: auto;
8041
- /* Scrolling past the end of this must not scroll the page behind it. */
8042
- overscroll-behavior: contain;
8043
8172
  font-size: 12px;
8044
8173
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
8045
8174
  background: var(--_assistant-bg);
@@ -8342,8 +8471,8 @@ var EMBEDDED_STYLES = `
8342
8471
  the widget lives in the host's own layout (fixes overlay/z-index clashes). */
8343
8472
  :host([placement="embedded"]) {
8344
8473
  --_position: var(--ag-ui-position, static);
8345
- --_width: var(--ag-ui-width, 100%);
8346
- --_height: var(--ag-ui-height, 100%);
8474
+ --_width: var(--_drag-width, var(--ag-ui-width, 100%));
8475
+ --_height: var(--_drag-height, var(--ag-ui-height, 100%));
8347
8476
  --_max-width: var(--ag-ui-max-width, 100%);
8348
8477
  --_max-height: var(--ag-ui-max-height, 100%);
8349
8478
  --_shadow: var(--ag-ui-shadow, none);
@@ -8601,7 +8730,7 @@ var PRESET_STYLES = `
8601
8730
 
8602
8731
  :host([placement="side"]) {
8603
8732
  --_inset: var(--ag-ui-inset, calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right) var(--_viewport-inset-bottom) auto);
8604
- --_width: var(--ag-ui-width, 420px);
8733
+ --_width: var(--_drag-width, var(--ag-ui-width, 420px));
8605
8734
  --_height: var(--ag-ui-height, var(--_viewport-height));
8606
8735
  --_max-height: var(--ag-ui-max-height, var(--_viewport-height));
8607
8736
  --_panel-radius: var(--ag-ui-panel-radius, 0);
@@ -8817,7 +8946,7 @@ var SIDEBAR_STYLES = `
8817
8946
  host-managed push instead. */
8818
8947
  :host([placement="sidebar"]) {
8819
8948
  --_inset: var(--ag-ui-inset, calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right) var(--_viewport-inset-bottom) auto);
8820
- --_width: var(--ag-ui-width, 420px);
8949
+ --_width: var(--_drag-width, var(--ag-ui-width, 420px));
8821
8950
  --_height: var(--ag-ui-height, var(--_viewport-height));
8822
8951
  --_max-height: var(--ag-ui-max-height, var(--_viewport-height));
8823
8952
  --_panel-radius: var(--ag-ui-panel-radius, 0);
@@ -8916,6 +9045,11 @@ var SMALL_VIEWPORT_STYLES = `
8916
9045
  fine-pointered and small, and conflating the two gets both wrong. What the
8917
9046
  pointer decides is which controls make sense, further down.
8918
9047
 
9048
+ The size the user dragged is deliberately not read here. The desktop
9049
+ placements read it before the host token, and this block restates the sizes
9050
+ without it, so a persisted or freshly dragged size cannot change the shape
9051
+ of a phone, and a window narrowed after a drag goes full-bleed.
9052
+
8919
9053
  Two placements are left alone. "page" is already this shape. "embedded" sits
8920
9054
  in a box the host sized and placed, and taking that over would break the app
8921
9055
  shell it was embedded into -- the host is the only party that knows whether
@@ -9057,8 +9191,6 @@ var THOUGHTS_STYLES = `
9057
9191
  border-left: 2px solid var(--_border);
9058
9192
  max-height: 220px;
9059
9193
  overflow: auto;
9060
- /* Scrolling past the end of this must not scroll the page behind it. */
9061
- overscroll-behavior: contain;
9062
9194
  white-space: pre-wrap;
9063
9195
  word-break: break-word;
9064
9196
  font-family: inherit;
@@ -9075,6 +9207,225 @@ var THOUGHTS_STYLES = `
9075
9207
  }
9076
9208
  `;
9077
9209
 
9210
+ // src/ui/progress/tool_group_styles.ts
9211
+ var CARD_MODES = [
9212
+ '[data-tool-display="inline"]',
9213
+ '[data-tool-display="minimal"]',
9214
+ '[data-tool-display="compact"]',
9215
+ '[data-tool-display="full"]'
9216
+ ].join(", ");
9217
+ var GROUPED = `:host(:where(:not(${CARD_MODES})))`;
9218
+ var CARDS = `:host(:is(${CARD_MODES}))`;
9219
+ var TOOL_GROUP_STYLES = `
9220
+ /* \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
9221
+ One quiet row per run of consecutive calls, the cards one click down as
9222
+ plain lines rather than boxes. Every rule is scoped to grouped mode, so
9223
+ none of it reaches a group drawn earlier once the host moves to a card
9224
+ mode. */
9225
+ ${GROUPED} .tool-group {
9226
+ align-self: stretch;
9227
+ display: flex;
9228
+ flex-direction: column;
9229
+ min-width: 0;
9230
+ }
9231
+
9232
+ ${GROUPED} .tool-group-head {
9233
+ appearance: none;
9234
+ align-self: flex-start;
9235
+ max-width: 100%;
9236
+ display: inline-flex;
9237
+ align-items: center;
9238
+ gap: 8px;
9239
+ margin: 0 0 0 -6px;
9240
+ padding: 4px 6px;
9241
+ border: 0;
9242
+ border-radius: 6px;
9243
+ background: transparent;
9244
+ color: var(--_muted);
9245
+ font: inherit;
9246
+ font-size: var(--_text-sm);
9247
+ line-height: 1.3;
9248
+ text-align: left;
9249
+ cursor: pointer;
9250
+ }
9251
+
9252
+ ${GROUPED} .tool-group-head:hover {
9253
+ background: var(--_hover);
9254
+ color: var(--_fg);
9255
+ }
9256
+
9257
+ ${GROUPED} .tool-group-head:focus-visible {
9258
+ outline: 2px solid var(--_ring);
9259
+ outline-offset: 1px;
9260
+ }
9261
+
9262
+ ${GROUPED} .tool-group-label {
9263
+ min-width: 0;
9264
+ font-weight: 500;
9265
+ color: var(--_ink-2);
9266
+ overflow-wrap: anywhere;
9267
+ }
9268
+
9269
+ ${GROUPED} .tool-group-meta {
9270
+ min-width: 0;
9271
+ color: var(--_muted);
9272
+ overflow-wrap: anywhere;
9273
+ }
9274
+
9275
+ /* Each note opens with a middle dot, which also parts the first note from
9276
+ the label. */
9277
+ ${GROUPED} .tool-group-note::before {
9278
+ content: "\\00B7";
9279
+ margin: 0 6px 0 2px;
9280
+ }
9281
+
9282
+ ${GROUPED} .tool-group[data-state="error"] .tool-group-meta {
9283
+ color: var(--_danger);
9284
+ }
9285
+
9286
+ ${GROUPED} .tool-group-icon {
9287
+ flex: none;
9288
+ box-sizing: border-box;
9289
+ width: 14px;
9290
+ height: 14px;
9291
+ background: currentColor;
9292
+ mask: var(--_ico-check) center / contain no-repeat;
9293
+ color: var(--_success);
9294
+ }
9295
+
9296
+ ${GROUPED} .tool-group[data-state="error"] .tool-group-icon {
9297
+ mask-image: var(--_ico-error);
9298
+ color: var(--_danger);
9299
+ }
9300
+
9301
+ ${GROUPED} .tool-group[data-state="running"] .tool-group-icon {
9302
+ mask: none;
9303
+ background: none;
9304
+ border: 2px solid var(--_muted);
9305
+ border-top-color: transparent;
9306
+ border-radius: 50%;
9307
+ animation: ag-ui-tool-spin var(--_tool-spin-duration) linear infinite;
9308
+ }
9309
+
9310
+ /* Waiting on the user: a steady accent dot rather than a spinner, and the
9311
+ count in the accent's ink. */
9312
+ ${GROUPED} .tool-group[data-state="waiting"] .tool-group-icon {
9313
+ mask: none;
9314
+ border-radius: 50%;
9315
+ transform: scale(0.6);
9316
+ background: var(--_accent);
9317
+ }
9318
+
9319
+ ${GROUPED} .tool-group[data-state="waiting"] .tool-group-meta {
9320
+ color: var(--_accent-ink);
9321
+ font-weight: 500;
9322
+ }
9323
+
9324
+ ${GROUPED} .tool-group-chevron {
9325
+ flex: none;
9326
+ width: 12px;
9327
+ height: 12px;
9328
+ background: currentColor;
9329
+ mask: var(--_ico-chevron) center / contain no-repeat;
9330
+ opacity: 0.7;
9331
+ transition: transform var(--_motion) var(--_ease);
9332
+ }
9333
+
9334
+ ${GROUPED} .tool-group[data-open="true"] .tool-group-chevron {
9335
+ transform: rotate(90deg);
9336
+ }
9337
+
9338
+ @media (prefers-reduced-motion: reduce) {
9339
+ ${GROUPED} .tool-group[data-state="running"] .tool-group-icon {
9340
+ animation: none;
9341
+ }
9342
+ }
9343
+
9344
+ ${GROUPED} .tool-group-list {
9345
+ display: flex;
9346
+ flex-direction: column;
9347
+ gap: 2px;
9348
+ margin: 2px 0 4px 6px;
9349
+ padding-left: 13px;
9350
+ border-left: 1px solid var(--_border);
9351
+ }
9352
+
9353
+ ${GROUPED} .tool-group[data-open="false"] .tool-group-list {
9354
+ display: none;
9355
+ }
9356
+
9357
+ /* A card inside the group is one line of the list: icon, name, and its
9358
+ Details control at the end, with the arguments and result opening under
9359
+ the whole row. */
9360
+ ${GROUPED} .tool-group .tool-call {
9361
+ max-width: 100%;
9362
+ align-self: stretch;
9363
+ flex-flow: row wrap;
9364
+ align-items: center;
9365
+ gap: 2px 10px;
9366
+ padding: 3px 0;
9367
+ border: 0;
9368
+ border-radius: 0;
9369
+ background: transparent;
9370
+ color: var(--_ink-2);
9371
+ font-family: inherit;
9372
+ font-size: var(--_text-sm);
9373
+ }
9374
+
9375
+ ${GROUPED} .tool-group .tool-call-head {
9376
+ flex: 1 1 auto;
9377
+ justify-content: flex-start;
9378
+ }
9379
+
9380
+ ${GROUPED} .tool-group .tool-call-name {
9381
+ flex: 0 1 auto;
9382
+ font-weight: 450;
9383
+ }
9384
+
9385
+ /* The repeat ordinal on a step whose label an earlier step has. */
9386
+ ${GROUPED} .tool-call-repeat {
9387
+ color: var(--_muted);
9388
+ font-variant-numeric: tabular-nums;
9389
+ }
9390
+
9391
+ ${GROUPED} .tool-group .tool-call-toggle {
9392
+ font-family: inherit;
9393
+ font-size: var(--_text-xs);
9394
+ color: var(--_muted);
9395
+ }
9396
+
9397
+ ${GROUPED} .tool-group .tool-call-body,
9398
+ ${GROUPED} .tool-group .tool-call-approval,
9399
+ ${GROUPED} .tool-group .tool-call-subagent {
9400
+ flex-basis: 100%;
9401
+ }
9402
+
9403
+ /* Grouping is decided as each card is built, so a card mode set afterwards
9404
+ finds groups already drawn. It flattens them: the group and its list stop
9405
+ generating boxes, the row and the repeat ordinals go, and every card lays
9406
+ out as a direct child of the answer, exactly as a card built in that mode
9407
+ does. */
9408
+ ${CARDS} .tool-group,
9409
+ ${CARDS} .tool-group-list {
9410
+ display: contents;
9411
+ }
9412
+
9413
+ ${CARDS} .tool-group-head,
9414
+ ${CARDS} .tool-call-repeat {
9415
+ display: none;
9416
+ }
9417
+
9418
+ /* Each of these declares its own display, which beats the user-agent rule for
9419
+ the hidden attribute, so each needs its own. The host prefix is there for
9420
+ weight, not scope: it lifts these over the card-mode rules above that set
9421
+ display, which are heavier than a bare class and attribute. */
9422
+ :host .tool-group[hidden],
9423
+ :host .tool-group-head[hidden],
9424
+ :host .tool-group-list[hidden] {
9425
+ display: none;
9426
+ }
9427
+ `;
9428
+
9078
9429
  // src/ui/progress/tool_step_styles.ts
9079
9430
  var TOOL_STEP_STYLES = `
9080
9431
  .tool-call {
@@ -9271,8 +9622,6 @@ var TOOL_STEP_STYLES = `
9271
9622
  padding: 6px 8px;
9272
9623
  max-height: 160px;
9273
9624
  overflow: auto;
9274
- /* Scrolling past the end of this must not scroll the page behind it. */
9275
- overscroll-behavior: contain;
9276
9625
  background: var(--_bg);
9277
9626
  border: 1px solid var(--_border);
9278
9627
  border-radius: 6px;
@@ -9718,6 +10067,7 @@ var TOKEN_STYLES = `
9718
10067
  --_line-strong: var(--ag-ui-line-strong, color-mix(in oklch, var(--_fg) 20%, var(--_bg)));
9719
10068
  --_wash: var(--ag-ui-wash, color-mix(in oklch, var(--_fg) 4.5%, var(--_bg)));
9720
10069
  --_accent-ink: var(--ag-ui-accent-ink, color-mix(in oklch, var(--_accent) 82%, var(--_fg)));
10070
+ --_ring: var(--ag-ui-ring, color-mix(in oklch, var(--_accent) 55%, transparent));
9721
10071
  /* The user's turn is the accent at a whisper. With Quiet's graphite accent
9722
10072
  that is a neutral wash; once a host sets a brand accent it becomes a tint
9723
10073
  of the brand, which is the one thing Soft does that Quiet keeps. */
@@ -9980,8 +10330,8 @@ var TOKEN_STYLES = `
9980
10330
  grid/flex layout) to embed it in the page flow instead of floating. */
9981
10331
  --_position: var(--ag-ui-position, fixed);
9982
10332
  --_z-index: var(--ag-ui-z-index, 2147483000);
9983
- --_width: var(--ag-ui-width, 380px);
9984
- --_height: var(--ag-ui-height, 560px);
10333
+ --_width: var(--_drag-width, var(--ag-ui-width, 380px));
10334
+ --_height: var(--_drag-height, var(--ag-ui-height, 560px));
9985
10335
  /* The gutter a resting floating panel keeps between itself and the edge of
9986
10336
  the box the host left free. One number, because the inset below spends it
9987
10337
  and the cap beneath has to know it was spent. */
@@ -10049,6 +10399,7 @@ var TOKEN_STYLES = `
10049
10399
  --_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
10400
  --_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
10401
  --_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"));
10402
+ --_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
10403
  }
10053
10404
 
10054
10405
  /* The colour drawn on the accent, from the accent's own luminance: white on
@@ -10267,6 +10618,21 @@ var LEDGER_STYLES = `
10267
10618
  letter-spacing: 0.01em;
10268
10619
  }
10269
10620
 
10621
+ /* The step group's row reads as a ledger heading. Unscoped to grouped mode:
10622
+ a card mode hides the row and drops the list's box, so neither reaches a
10623
+ flattened card. */
10624
+ :host([theme~="ledger"]) .tool-group-head {
10625
+ font-family: var(--_code-font);
10626
+ font-size: var(--_text-xs);
10627
+ border-radius: 2px;
10628
+ text-transform: uppercase;
10629
+ letter-spacing: 0.06em;
10630
+ }
10631
+
10632
+ :host([theme~="ledger"]) .tool-group-list {
10633
+ border-left: 1px dashed var(--_line-strong);
10634
+ }
10635
+
10270
10636
  :host([theme~="ledger"]) .chart-block {
10271
10637
  border-top: 1px solid var(--_line-strong);
10272
10638
  border-bottom: 1px solid var(--_line);
@@ -10650,6 +11016,26 @@ var SOFT_STYLES = `
10650
11016
  color: var(--_accent-ink);
10651
11017
  }
10652
11018
 
11019
+ /* The step group's row as a tinted pill. Unscoped to grouped mode: a card
11020
+ mode hides the row and drops the list's box, so neither reaches a
11021
+ flattened card. */
11022
+ :host([theme~="soft"]) .tool-group-head {
11023
+ background: var(--_tint);
11024
+ border: 1px solid var(--_border);
11025
+ border-radius: 999px;
11026
+ padding: 5px 12px 5px 9px;
11027
+ margin-left: 0;
11028
+ }
11029
+
11030
+ :host([theme~="soft"]) .tool-group-head:hover {
11031
+ background: var(--_tint-2);
11032
+ }
11033
+
11034
+ :host([theme~="soft"]) .tool-group-list {
11035
+ border-left-color: var(--_tint-2);
11036
+ margin-left: 14px;
11037
+ }
11038
+
10653
11039
  :host([theme~="soft"]) .chart-block {
10654
11040
  background: var(--_bg);
10655
11041
  border: 1px solid var(--_border);
@@ -11318,8 +11704,6 @@ var MESSAGE_STYLES = `
11318
11704
  .message--assistant pre {
11319
11705
  padding: 8px 10px;
11320
11706
  overflow: auto;
11321
- /* Scrolling past the end of this must not scroll the page behind it. */
11322
- overscroll-behavior: contain;
11323
11707
  background: var(--_bg);
11324
11708
  border: 1px solid var(--_border);
11325
11709
  border-radius: 6px;
@@ -11627,6 +12011,10 @@ var STYLES = [
11627
12011
  QUESTION_STYLES,
11628
12012
  SKILL_STYLES,
11629
12013
  HISTORY_STYLES,
12014
+ // The step group restates, inside a group, card rules the modules above set
12015
+ // at rest; it outweighs them rather than relying on order, and sits ahead
12016
+ // of the themes so a theme restating a group rule at equal weight wins.
12017
+ TOOL_GROUP_STYLES,
11630
12018
  // Each theme's structure comes last: it restates rules the modules above
11631
12019
  // set at rest, at equal weight. Classic and code have no structural
11632
12020
  // module; they are the base stylesheet, and classic adds only its
@@ -15854,6 +16242,201 @@ var ThoughtsBlock = class {
15854
16242
  }
15855
16243
  };
15856
16244
 
16245
+ // src/ui/progress/tool_step_group.ts
16246
+ var OPEN_STATUSES = /* @__PURE__ */ new Set([
16247
+ TOOL_CALL_STATUS.PENDING,
16248
+ TOOL_CALL_STATUS.DEFERRED
16249
+ ]);
16250
+ var RUNNING_SUBAGENT = `.tool-call-subagent > .subagent:is(${[
16251
+ SUBAGENT_PHASE.STARTED,
16252
+ SUBAGENT_PHASE.TOOL_CALL,
16253
+ SUBAGENT_PHASE.TOOL_RESULT
16254
+ ].map((phase) => `[data-phase="${phase}"]`).join(", ")})`;
16255
+ var WATCHED_ATTRIBUTES = [
16256
+ "data-status",
16257
+ "data-asking",
16258
+ "data-phase",
16259
+ "data-agent"
16260
+ ];
16261
+ var ToolStepGroup = class {
16262
+ element;
16263
+ #strings;
16264
+ #list;
16265
+ #head;
16266
+ #label;
16267
+ #meta;
16268
+ /** The steps, in order. Cards only ever join a group, so this is its list's children. */
16269
+ #steps = [];
16270
+ /** Whether the group has already opened itself for the current wait. */
16271
+ #openedForWait = false;
16272
+ constructor(strings) {
16273
+ this.#strings = strings;
16274
+ this.element = document.createElement("div");
16275
+ this.element.className = "tool-group";
16276
+ this.element.setAttribute("part", "tool-group");
16277
+ this.element.setAttribute("data-open", "false");
16278
+ this.#head = document.createElement("button");
16279
+ this.#head.type = "button";
16280
+ this.#head.className = "tool-group-head";
16281
+ this.#head.setAttribute("part", "tool-group-head");
16282
+ this.#head.setAttribute("aria-expanded", "false");
16283
+ const icon = document.createElement("span");
16284
+ icon.className = "tool-group-icon";
16285
+ icon.setAttribute("part", "tool-group-icon");
16286
+ icon.setAttribute("aria-hidden", "true");
16287
+ this.#label = document.createElement("span");
16288
+ this.#label.className = "tool-group-label";
16289
+ this.#label.setAttribute("part", "tool-group-label");
16290
+ this.#meta = document.createElement("span");
16291
+ this.#meta.className = "tool-group-meta";
16292
+ this.#meta.setAttribute("part", "tool-group-meta");
16293
+ const chevron = document.createElement("span");
16294
+ chevron.className = "tool-group-chevron";
16295
+ chevron.setAttribute("aria-hidden", "true");
16296
+ this.#head.append(icon, this.#label, this.#meta, chevron);
16297
+ this.#head.addEventListener("click", () => {
16298
+ this.setOpen(this.element.getAttribute("data-open") !== "true");
16299
+ });
16300
+ this.#list = document.createElement("div");
16301
+ this.#list.className = "tool-group-list";
16302
+ this.#list.setAttribute("part", "tool-group-list");
16303
+ this.element.append(this.#head, this.#list);
16304
+ new MutationObserver(() => {
16305
+ this.#refresh();
16306
+ }).observe(this.#list, {
16307
+ attributes: true,
16308
+ attributeFilter: [...WATCHED_ATTRIBUTES],
16309
+ subtree: true
16310
+ });
16311
+ }
16312
+ /**
16313
+ * Add a card's element as the group's next step, under the label the card
16314
+ * was built with.
16315
+ *
16316
+ * A step whose label an earlier step already has gets a repeat ordinal,
16317
+ * "Fill field (2)", in an element of its own beside the card's name. The
16318
+ * name itself is never rewritten, so a group flattened back into per-call
16319
+ * cards by a card display mode shows exactly the label the card was built
16320
+ * with, and the CSS hides the ordinal there.
16321
+ */
16322
+ add(card, name) {
16323
+ const seen = this.#steps.filter((step) => step.name === name).length + 1;
16324
+ let label = name;
16325
+ if (seen > 1) {
16326
+ const repeat = document.createElement("span");
16327
+ repeat.className = "tool-call-repeat";
16328
+ repeat.setAttribute("part", "tool-card-repeat");
16329
+ repeat.textContent = fillUiString(this.#strings.toolStepRepeat, { n: seen });
16330
+ card.querySelector(".tool-call-name")?.after(repeat);
16331
+ label = `${name} ${repeat.textContent}`;
16332
+ }
16333
+ this.#steps.push({ element: card, name, label });
16334
+ this.#list.append(card);
16335
+ this.#refresh();
16336
+ }
16337
+ /** Show or hide the per-step cards. */
16338
+ setOpen(open) {
16339
+ this.element.setAttribute("data-open", String(open));
16340
+ this.#head.setAttribute("aria-expanded", String(open));
16341
+ }
16342
+ /**
16343
+ * Open the group now, for the wait that is about to start.
16344
+ *
16345
+ * The group opens itself from its mutation observer, which runs a tick after
16346
+ * the card is marked deferred. The approval prompt is drawn, and takes focus,
16347
+ * in the same tick as the mark, while the list is still out of the layout: the
16348
+ * focus is dropped and the scroll it would cause never happens. So the caller
16349
+ * that is about to draw the prompt opens the group first, and this marks the
16350
+ * wait as already answered for, so the observer's later refresh does not open
16351
+ * it again over a user who folded it.
16352
+ */
16353
+ reveal() {
16354
+ this.#openedForWait = true;
16355
+ this.setOpen(true);
16356
+ }
16357
+ /**
16358
+ * The row's note for a running sub-agent, naming it, or `null` when no step
16359
+ * is delegating right now. Only a step that is itself still running counts.
16360
+ */
16361
+ #delegation() {
16362
+ for (const { element } of this.#steps) {
16363
+ const panel = element.getAttribute("data-status") === TOOL_CALL_STATUS.PENDING ? element.querySelector(RUNNING_SUBAGENT) : null;
16364
+ if (panel !== null) {
16365
+ const agent = panel.getAttribute("data-agent");
16366
+ return agent === null ? this.#strings.subAgentWorking : fillUiString(this.#strings.subAgentDelegatedTo, { agent });
16367
+ }
16368
+ }
16369
+ return null;
16370
+ }
16371
+ #refresh() {
16372
+ const strings = this.#strings;
16373
+ const steps = this.#steps;
16374
+ const statuses = steps.map((step) => step.element.getAttribute("data-status") ?? "");
16375
+ const open = steps.filter((_2, i) => OPEN_STATUSES.has(statuses[i]));
16376
+ const count = (status) => statuses.filter((s) => s === status).length;
16377
+ const failed = count(TOOL_CALL_STATUS.ERROR);
16378
+ const declined = count(TOOL_CALL_STATUS.DECLINED);
16379
+ const deferred = count(TOOL_CALL_STATUS.DEFERRED);
16380
+ const asking = steps.filter((step) => step.element.hasAttribute("data-asking")).length;
16381
+ const waiting = deferred + asking;
16382
+ const total = steps.length;
16383
+ if (deferred > 0 && !this.#openedForWait) {
16384
+ this.#openedForWait = true;
16385
+ this.setOpen(true);
16386
+ } else if (deferred === 0) {
16387
+ this.#openedForWait = false;
16388
+ }
16389
+ let state = "done";
16390
+ if (waiting > 0) {
16391
+ state = "waiting";
16392
+ } else if (open.length > 0) {
16393
+ state = "running";
16394
+ } else if (failed > 0) {
16395
+ state = "error";
16396
+ }
16397
+ this.element.setAttribute("data-state", state);
16398
+ this.element.setAttribute("part", `tool-group tool-group-${state}`);
16399
+ this.element.setAttribute("data-count", String(total));
16400
+ const only = total === 1 ? steps[0] : void 0;
16401
+ if (only !== void 0) {
16402
+ this.#label.textContent = only.label;
16403
+ } else {
16404
+ const template = state === "waiting" ? strings.toolGroupSteps : state === "running" ? strings.toolGroupWorking : strings.toolGroupWorked;
16405
+ this.#label.textContent = fillUiString(template, { count: total });
16406
+ }
16407
+ const notes = [];
16408
+ const delegation = this.#delegation();
16409
+ const current = open[0];
16410
+ if (waiting > 0) {
16411
+ notes.push(fillUiString(strings.toolGroupWaiting, { count: waiting }));
16412
+ } else if (delegation !== null) {
16413
+ notes.push(delegation);
16414
+ } else if (current !== void 0 && total > 1) {
16415
+ notes.push(current.label);
16416
+ }
16417
+ if (failed > 0) {
16418
+ notes.push(fillUiString(strings.toolGroupFailed, { count: failed }));
16419
+ }
16420
+ if (declined > 0) {
16421
+ notes.push(fillUiString(strings.toolGroupDeclined, { count: declined }));
16422
+ }
16423
+ this.#meta.replaceChildren(
16424
+ ...notes.map((text4) => {
16425
+ const note = document.createElement("span");
16426
+ note.className = "tool-group-note";
16427
+ note.textContent = text4;
16428
+ return note;
16429
+ })
16430
+ );
16431
+ this.#meta.hidden = notes.length === 0;
16432
+ }
16433
+ };
16434
+
16435
+ // src/ui/transcript/groups_tool_steps.ts
16436
+ function groupsToolSteps(mode) {
16437
+ return mode === TOOL_DISPLAY.GROUPED;
16438
+ }
16439
+
15857
16440
  // src/ui/transcript/render_or_warn.ts
15858
16441
  function renderOrWarn(render, subject) {
15859
16442
  try {
@@ -15960,6 +16543,12 @@ var Transcript = class {
15960
16543
  #toolCards = /* @__PURE__ */ new Map();
15961
16544
  /** Card elements by call id, so a rendering handler can find its own card. */
15962
16545
  #cardElements = /* @__PURE__ */ new Map();
16546
+ /**
16547
+ * Cards whose call is being run, by call id. A card leaves the awaiting ones
16548
+ * the moment its call is dispatched, and a handler that opens a prompt on its
16549
+ * own card is still running long after, so it is found here.
16550
+ */
16551
+ #running = /* @__PURE__ */ new Map();
15963
16552
  /**
15964
16553
  * Call ids whose card was already settled from a streamed server-side result
15965
16554
  * (`TOOL_CALL_RESULT`), so the post-run executeTool sweep doesn't overwrite
@@ -15980,6 +16569,24 @@ var Transcript = class {
15980
16569
  // the answer group while a reasoning model thinks and collapsed once the
15981
16570
  // answer's first text token arrives. `null` outside a reasoning turn.
15982
16571
  #thoughts = null;
16572
+ /** The step group behind each group element, so a new card can find its run. */
16573
+ #stepGroups = /* @__PURE__ */ new WeakMap();
16574
+ /**
16575
+ * An empty comment after each step group that has rendered output, which
16576
+ * every later output is placed before, so a group's outputs come out in call
16577
+ * order. Every card of a round joins its group before any handler runs, so
16578
+ * placing each output straight after the group would put the newest first;
16579
+ * and a marker rather than the last output, because an output may be a
16580
+ * fragment, which leaves nothing behind to place the next one after.
16581
+ */
16582
+ #groupEnds = /* @__PURE__ */ new WeakMap();
16583
+ /**
16584
+ * The step group element each placed output belongs to. A tool's own output
16585
+ * does not end its group: the next call joins the group above it. Without
16586
+ * that, replay -- which draws each restored call's output before the next
16587
+ * call's card -- would split a run the live stream drew as one group.
16588
+ */
16589
+ #outputGroups = /* @__PURE__ */ new WeakMap();
15983
16590
  constructor(host) {
15984
16591
  this.#host = host;
15985
16592
  }
@@ -16075,6 +16682,18 @@ var Transcript = class {
16075
16682
  forgetCards() {
16076
16683
  this.#serverSettled.clear();
16077
16684
  this.#cardElements.clear();
16685
+ this.#running.clear();
16686
+ }
16687
+ /** Remember the card of a call being run, for {@link setAsking}. */
16688
+ holdRunning(callId, card) {
16689
+ this.#running.set(callId, card);
16690
+ }
16691
+ /**
16692
+ * Say whether a prompt is open on the card of a running call. A call id of
16693
+ * none, or one no card was held for, has no card to say it on.
16694
+ */
16695
+ setAsking(callId, asking) {
16696
+ this.#running.get(callId)?.setAsking(asking);
16078
16697
  }
16079
16698
  /** Empty the list back to its empty-state region. The last half of wiping it. */
16080
16699
  empty() {
@@ -16248,7 +16867,23 @@ var Transcript = class {
16248
16867
  if (node === null) {
16249
16868
  return;
16250
16869
  }
16251
- this.#cardElements.get(call.id)?.after(node);
16870
+ const anchor = this.#cardElements.get(call.id);
16871
+ const group = anchor?.closest(".tool-group");
16872
+ if (group === null || group === void 0) {
16873
+ anchor?.after(node);
16874
+ } else {
16875
+ let end = this.#groupEnds.get(group);
16876
+ if (end === void 0) {
16877
+ end = document.createComment("");
16878
+ group.after(end);
16879
+ this.#groupEnds.set(group, end);
16880
+ }
16881
+ const placed = node instanceof DocumentFragment ? [...node.children] : [node];
16882
+ end.before(node);
16883
+ for (const output of placed) {
16884
+ this.#outputGroups.set(output, group);
16885
+ }
16886
+ }
16252
16887
  this.afterGrew();
16253
16888
  }
16254
16889
  /** Update the empty state and follow the foot, after the transcript grew. */
@@ -16276,11 +16911,49 @@ var Transcript = class {
16276
16911
  formatPayload: (payload) => this.#host.formatToolPayload(payload)
16277
16912
  });
16278
16913
  this.#toolCards.set(call.id, card);
16279
- this.ensureGroup().appendChild(card.element);
16914
+ if (groupsToolSteps(this.#host.toolDisplay())) {
16915
+ this.#stepGroup().add(card.element, summary);
16916
+ } else {
16917
+ this.ensureGroup().appendChild(card.element);
16918
+ }
16280
16919
  this.updateEmptyState();
16281
16920
  this.follow();
16282
16921
  return card;
16283
16922
  }
16923
+ /**
16924
+ * Open the step group ``card`` sits in, at once, ahead of a prompt about to be
16925
+ * drawn inside it. A no-op for a card drawn in a card display mode, which has
16926
+ * no group.
16927
+ */
16928
+ revealStep(card) {
16929
+ const group = card.element.closest(".tool-group");
16930
+ (group === null ? void 0 : this.#stepGroups.get(group))?.reveal();
16931
+ }
16932
+ /**
16933
+ * The step group a new card joins. Consecutive cards share one, and so do
16934
+ * cards separated only by the group's own rendered output; anything else in
16935
+ * between (text, a confirmation, a question) starts the next.
16936
+ *
16937
+ * The pending dots are not skipped over: a tool call takes them down before
16938
+ * its card is drawn, so they are never what a card finds at the foot of the
16939
+ * turn.
16940
+ */
16941
+ #stepGroup() {
16942
+ const answer = this.ensureGroup();
16943
+ const last = answer.lastElementChild;
16944
+ const existing = last === null ? void 0 : this.#stepGroups.get(this.#outputGroups.get(last) ?? last);
16945
+ if (existing !== void 0) {
16946
+ return existing;
16947
+ }
16948
+ const group = new ToolStepGroup(this.#host.strings());
16949
+ this.#stepGroups.set(group.element, group);
16950
+ if (last === null) {
16951
+ answer.prepend(group.element);
16952
+ } else {
16953
+ last.after(group.element);
16954
+ }
16955
+ return group;
16956
+ }
16284
16957
  };
16285
16958
 
16286
16959
  // src/core/activity_registry.ts
@@ -17429,8 +18102,13 @@ var RunHandlers = class {
17429
18102
  * A handler table for one AG-UI client. Every table built for the current
17430
18103
  * conversation draws into the same transcript and shares its bookkeeping, so
17431
18104
  * a continuation run reports into the interaction the user is looking at.
18105
+ *
18106
+ * `threadId` is the thread the client was built for, named on the
18107
+ * run-finished detail. Taken here rather than read when the run settles,
18108
+ * because a stopped run settles after the reset that stopped it, when the
18109
+ * thread on screen is already the next one.
17432
18110
  */
17433
- forClient() {
18111
+ forClient(threadId) {
17434
18112
  const ledger = this.#ledger;
17435
18113
  const attached = () => ledger === this.#ledger;
17436
18114
  const { onSettled, ...drawing } = this.#tableFor(ledger);
@@ -17440,7 +18118,7 @@ var RunHandlers = class {
17440
18118
  if (attached()) {
17441
18119
  onSettled();
17442
18120
  }
17443
- this.#dispatchRunFinished(ledger);
18121
+ this.#dispatchRunFinished(ledger, threadId);
17444
18122
  }
17445
18123
  };
17446
18124
  }
@@ -17593,7 +18271,7 @@ var RunHandlers = class {
17593
18271
  * server is a fact about the run, and a name can appear on both sides across a
17594
18272
  * conversation.
17595
18273
  */
17596
- #dispatchRunFinished(ledger) {
18274
+ #dispatchRunFinished(ledger, threadId) {
17597
18275
  const tools = ledger.tools.map(({ id, name }) => ({
17598
18276
  name,
17599
18277
  side: ledger.serverSettled.has(id) ? "server" : "client"
@@ -17604,7 +18282,7 @@ var RunHandlers = class {
17604
18282
  ledger.invalidated.clear();
17605
18283
  this.#host.element.dispatchEvent(
17606
18284
  new CustomEvent(RUN_FINISHED_EVENT, {
17607
- detail: { tools, invalidated },
18285
+ detail: { threadId, tools, invalidated },
17608
18286
  bubbles: true,
17609
18287
  composed: true
17610
18288
  })
@@ -18423,6 +19101,11 @@ var AgUiChat = class extends HTMLElement {
18423
19101
  * then a host's {@link sendMessage} waits for it rather than refusing.
18424
19102
  */
18425
19103
  #restoreHold = null;
19104
+ /**
19105
+ * The thread the latest {@link THREAD_EVENT} reported, which the next one
19106
+ * names as the thread it replaced; `null` before the first.
19107
+ */
19108
+ #shownThread = null;
18426
19109
  /** The decision a run is suspended on, which a Stop abandons. */
18427
19110
  #decision = new PendingDecision();
18428
19111
  /**
@@ -18440,6 +19123,7 @@ var AgUiChat = class extends HTMLElement {
18440
19123
  askUser: () => this.askUser,
18441
19124
  askUserRenderer: () => this.askUserRenderer,
18442
19125
  decision: this.#decision,
19126
+ setAsking: (callId, asking) => this.#transcript.setAsking(callId, asking),
18443
19127
  ensureGroup: () => this.#transcript.ensureGroup(),
18444
19128
  strings: () => this.#strings,
18445
19129
  hidePending: () => this.#transcript.hidePending(),
@@ -18453,6 +19137,8 @@ var AgUiChat = class extends HTMLElement {
18453
19137
  allowImages: () => this.allowImages,
18454
19138
  follow: () => this.#transcript.follow()
18455
19139
  });
19140
+ // This chat's place among the connected ones, which a free highlight ring reads.
19141
+ #chatRegistration = new ChatRegistration(this);
18456
19142
  /**
18457
19143
  * Which storage keys are this element's: the namespace it claims, the keys
18458
19144
  * its layout preferences live under, and the built-in store scoped to them.
@@ -18492,6 +19178,7 @@ var AgUiChat = class extends HTMLElement {
18492
19178
  resolveTool: (name) => this.#tools.resolve(name),
18493
19179
  toolSummaries: () => this.toolSummaries,
18494
19180
  serverSummary: (name) => this.#tools.summary(name),
19181
+ toolDisplay: () => this.toolDisplay,
18495
19182
  // A thunk over the live property, not the property itself: the card keeps
18496
19183
  // this for the life of the call, and the result region is filled when the
18497
19184
  // tool settles -- which can be long after a host set the hook.
@@ -18699,7 +19386,8 @@ var AgUiChat = class extends HTMLElement {
18699
19386
  },
18700
19387
  cancelRun: () => this.#cancelRun(),
18701
19388
  resetState: () => this.#resetState(),
18702
- setRunning: (running) => this.#setRunning(running)
19389
+ setRunning: (running) => this.#setRunning(running),
19390
+ threadChanged: (threadId, reason) => this.#dispatchThread(threadId, reason)
18703
19391
  });
18704
19392
  }
18705
19393
  /** Attributes the element reacts to after it has been connected. */
@@ -18963,18 +19651,23 @@ var AgUiChat = class extends HTMLElement {
18963
19651
  }
18964
19652
  /**
18965
19653
  * How much detail tool-call cards show, from the `data-tool-display`
18966
- * attribute (`minimal` / `inline` / `compact` / `full`). Defaults to `full`.
19654
+ * attribute (`grouped` / `minimal` / `inline` / `compact` / `full`).
19655
+ * Defaults to `grouped`, for an absent attribute and for a value the
19656
+ * element does not know; `full` is the per-call card default before
19657
+ * `grouped` existed.
18967
19658
  *
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.
19659
+ * The four card modes are applied by the shadow CSS from the attribute
19660
+ * itself, so changing between them restyles every card already in the
19661
+ * transcript. Whether calls fold into a step group is decided as each card
19662
+ * is built: switching to `grouped` folds only the calls drawn afterwards,
19663
+ * and switching away flattens the existing groups, again by CSS.
18971
19664
  */
18972
19665
  get toolDisplay() {
18973
19666
  const attr = this.getAttribute("data-tool-display");
18974
- if (attr === TOOL_DISPLAY.INLINE || attr === TOOL_DISPLAY.MINIMAL || attr === TOOL_DISPLAY.COMPACT) {
19667
+ if (attr === TOOL_DISPLAY.INLINE || attr === TOOL_DISPLAY.MINIMAL || attr === TOOL_DISPLAY.COMPACT || attr === TOOL_DISPLAY.FULL) {
18975
19668
  return attr;
18976
19669
  }
18977
- return TOOL_DISPLAY.FULL;
19670
+ return TOOL_DISPLAY.GROUPED;
18978
19671
  }
18979
19672
  set toolDisplay(value) {
18980
19673
  this.setAttribute("data-tool-display", value);
@@ -19007,6 +19700,7 @@ var AgUiChat = class extends HTMLElement {
19007
19700
  }
19008
19701
  this.#connectedBefore = true;
19009
19702
  this.#storage.claim();
19703
+ this.#chatRegistration.join();
19010
19704
  this.#placement.restoreSize();
19011
19705
  requestAnimationFrame(() => {
19012
19706
  this.#placement.restoreLauncherPosition();
@@ -19044,7 +19738,7 @@ var AgUiChat = class extends HTMLElement {
19044
19738
  this.#voice.wire();
19045
19739
  this.#history.adoptActiveThread();
19046
19740
  queueMicrotask(() => this.#startup());
19047
- void this.#history.rehydrate();
19741
+ void this.#history.rehydrate(changed && held !== "" ? "rescope" : "restore");
19048
19742
  if (changed) {
19049
19743
  void this.#history.refreshDrawer();
19050
19744
  }
@@ -19097,7 +19791,7 @@ var AgUiChat = class extends HTMLElement {
19097
19791
  await Promise.all([
19098
19792
  this.#tools.fetchCatalog(),
19099
19793
  this.#skills.fetch(),
19100
- this.#history.rehydrate()
19794
+ this.#history.rehydrate("restore")
19101
19795
  ]);
19102
19796
  }
19103
19797
  /**
@@ -19113,6 +19807,7 @@ var AgUiChat = class extends HTMLElement {
19113
19807
  window.visualViewport?.removeEventListener("resize", this.#onViewportResize);
19114
19808
  window.visualViewport?.removeEventListener("scroll", this.#onViewportResize);
19115
19809
  this.#storage.release();
19810
+ this.#chatRegistration.leave();
19116
19811
  this.#cancelRun();
19117
19812
  this.#excerpts.detachPageOffer();
19118
19813
  this.#attachments.tray?.dispose();
@@ -19260,6 +19955,26 @@ var AgUiChat = class extends HTMLElement {
19260
19955
  get unread() {
19261
19956
  return this.#unread;
19262
19957
  }
19958
+ /**
19959
+ * The thread on screen; `null` before the element first connects.
19960
+ *
19961
+ * Only reads. `conversationStore.threadId()` mints and stores a thread when
19962
+ * the store points at none, so a host asking which conversation is showing
19963
+ * could start one; this never does. {@link THREAD_EVENT} says when it changes.
19964
+ */
19965
+ get threadId() {
19966
+ const threadId = this.#history.threadId;
19967
+ return threadId === "" ? null : threadId;
19968
+ }
19969
+ /**
19970
+ * Whether an interaction is in flight: the composer's own state, the one that
19971
+ * shows Stop. It spans every tool round, a retry and a checkpoint
19972
+ * continuation as well as a send, which {@link SUBMIT_EVENT} alone does not.
19973
+ * Reflected as `data-running` on the element, for a host's CSS.
19974
+ */
19975
+ get running() {
19976
+ return this.#running;
19977
+ }
19263
19978
  /**
19264
19979
  * Whether to mount collapsed.
19265
19980
  *
@@ -19310,6 +20025,13 @@ var AgUiChat = class extends HTMLElement {
19310
20025
  moveTo(corner, options = {}) {
19311
20026
  return this.#placement.moveTo(corner, options);
19312
20027
  }
20028
+ /**
20029
+ * Drop a size the user dragged, and its stored copy, so the placement's
20030
+ * default and the host's `--ag-ui-width` / `--ag-ui-height` apply again.
20031
+ */
20032
+ resetSize() {
20033
+ this.#placement.resetSize();
20034
+ }
19313
20035
  /** Flip the collapsed state. Bound to the built-in header toggle. */
19314
20036
  toggleCollapsed() {
19315
20037
  this.setCollapsed(!this.collapsed);
@@ -19379,7 +20101,7 @@ var AgUiChat = class extends HTMLElement {
19379
20101
  this.#voice.dispose();
19380
20102
  this.#voice.wire();
19381
20103
  this.#history.adoptActiveThread();
19382
- void this.#history.rehydrate();
20104
+ void this.#history.rehydrate("rescope");
19383
20105
  void this.#history.refreshDrawer();
19384
20106
  }
19385
20107
  /**
@@ -19440,6 +20162,34 @@ var AgUiChat = class extends HTMLElement {
19440
20162
  closeCheckpoints() {
19441
20163
  this.#checkpoints.close();
19442
20164
  }
20165
+ /**
20166
+ * Put a stored thread on screen, as picking it in the conversation list does,
20167
+ * for a host that lists conversations in its own chrome.
20168
+ *
20169
+ * Resolves once the thread has been replayed, when {@link THREAD_EVENT} has
20170
+ * fired. **A run in flight is cancelled**, as the list and {@link newChat} do;
20171
+ * a host that would rather wait reads {@link running} first. The thread
20172
+ * already on screen is left alone, and so is everything before the element
20173
+ * connects, since until then it has no store to read the thread from.
20174
+ */
20175
+ async switchThread(threadId) {
20176
+ if (!this.#connected) {
20177
+ return;
20178
+ }
20179
+ await this.#history.switchThread(threadId);
20180
+ }
20181
+ /**
20182
+ * Delete a stored thread, as the conversation list's delete does. Deleting the
20183
+ * thread on screen cancels its run and puts a fresh thread in its place, and
20184
+ * {@link THREAD_EVENT} reports it; deleting another leaves the screen alone.
20185
+ * Does nothing before the element connects.
20186
+ */
20187
+ deleteThread(threadId) {
20188
+ if (!this.#connected) {
20189
+ return;
20190
+ }
20191
+ this.#history.deleteThread(threadId);
20192
+ }
19443
20193
  /**
19444
20194
  * Open the checkpoints panel, or close it if it is already open — what the
19445
20195
  * built-in ⭯ button does, because a control that opens a panel is read as the
@@ -19472,6 +20222,7 @@ var AgUiChat = class extends HTMLElement {
19472
20222
  this.#history.startThread();
19473
20223
  this.#setRunning(false);
19474
20224
  this.#setUnread(0);
20225
+ this.#dispatchThread(this.#history.threadId, "new");
19475
20226
  if (!this.collapsed) {
19476
20227
  this.#input.focus({ preventScroll: true });
19477
20228
  }
@@ -19664,6 +20415,8 @@ var AgUiChat = class extends HTMLElement {
19664
20415
  },
19665
20416
  onFilesPicked: () => this.#attachments.onFilesPicked()
19666
20417
  });
20418
+ const aboveComposer = document.createElement("slot");
20419
+ aboveComposer.name = "above-composer";
19667
20420
  const footer = document.createElement("slot");
19668
20421
  footer.name = "footer";
19669
20422
  this.#messagesWrap.className = "messages-wrap";
@@ -19676,6 +20429,7 @@ var AgUiChat = class extends HTMLElement {
19676
20429
  this.#composerHint,
19677
20430
  this.#queuedRow,
19678
20431
  this.#attachSlot,
20432
+ aboveComposer,
19679
20433
  inputRow,
19680
20434
  footer,
19681
20435
  this.#drawer.element,
@@ -19843,6 +20597,21 @@ var AgUiChat = class extends HTMLElement {
19843
20597
  this.#sending = null;
19844
20598
  this.#history.stopContinuation();
19845
20599
  }
20600
+ /**
20601
+ * Say the conversation on screen changed: {@link THREAD_EVENT}.
20602
+ *
20603
+ * The thread it replaced is the one last reported, rather than the one the
20604
+ * element last pointed at: a switch superseded before it replayed never drew
20605
+ * its thread, so naming it as the previous one left a host's own list
20606
+ * marking a thread that was never shown and still marking the one that was.
20607
+ */
20608
+ #dispatchThread(threadId, reason) {
20609
+ const detail = { threadId, previousThreadId: this.#shownThread, reason };
20610
+ this.#shownThread = threadId;
20611
+ this.dispatchEvent(
20612
+ new CustomEvent(THREAD_EVENT, { detail, bubbles: true, composed: true })
20613
+ );
20614
+ }
19846
20615
  /**
19847
20616
  * Swap the composer button between Send (idle) and Stop (running).
19848
20617
  *
@@ -19860,6 +20629,7 @@ var AgUiChat = class extends HTMLElement {
19860
20629
  this.#send.title = label;
19861
20630
  this.#send.setAttribute("aria-label", label);
19862
20631
  this.#send.dataset["state"] = running ? "running" : "idle";
20632
+ this.toggleAttribute("data-running", running);
19863
20633
  if (settled) {
19864
20634
  this.#flushQueued();
19865
20635
  }
@@ -20148,7 +20918,7 @@ var AgUiChat = class extends HTMLElement {
20148
20918
  });
20149
20919
  return new AgUiClient({
20150
20920
  agent,
20151
- handlers: this.#runHandlers.forClient(),
20921
+ handlers: this.#runHandlers.forClient(threadId),
20152
20922
  getTools: () => this.#tools.advertise(),
20153
20923
  getContext: () => this.#dispatch.buildContext(),
20154
20924
  executeTool: (call) => this.#dispatch.execute(call),
@@ -20342,7 +21112,7 @@ function setControlValue(el2, value) {
20342
21112
  }
20343
21113
 
20344
21114
  // src/version.ts
20345
- var VERSION = "0.45.0";
21115
+ var VERSION = "0.47.0";
20346
21116
  export {
20347
21117
  ATTACHMENT_EVENT,
20348
21118
  AgUiChat,
@@ -20375,6 +21145,7 @@ export {
20375
21145
  SUBMIT_EVENT,
20376
21146
  SUGGESTIONS_ACTIVITY_TYPE,
20377
21147
  SessionStorageStore,
21148
+ THREAD_EVENT,
20378
21149
  TOGGLE_EVENT,
20379
21150
  TOOL_CALL_STATUS,
20380
21151
  TOOL_DISPLAY,