@artooi/ag-ui-web-component 0.46.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 (54) hide show
  1. package/CHANGELOG.md +87 -1
  2. package/README.md +130 -19
  3. package/dist/ag-ui-web-component.bundle.js +99 -94
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +10 -0
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +38 -0
  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 +249 -54
  26. package/dist/index.js.map +4 -4
  27. package/dist/ui/history/conversation_history.d.ts +10 -1
  28. package/dist/ui/history/conversation_history.d.ts.map +1 -1
  29. package/dist/ui/placement/panel_placement.d.ts +6 -0
  30. package/dist/ui/placement/panel_placement.d.ts.map +1 -1
  31. package/dist/ui/placement/small_viewport_styles.d.ts.map +1 -1
  32. package/dist/ui/progress/tool_call_card.d.ts.map +1 -1
  33. package/dist/ui/progress/tool_step_group.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/constants.ts +11 -0
  36. package/src/core/ag_ui_chat.ts +108 -5
  37. package/src/core/chat_registry.ts +53 -0
  38. package/src/core/events/run_finished_detail.ts +6 -0
  39. package/src/core/events/thread_detail.ts +26 -0
  40. package/src/core/run_handlers.ts +9 -4
  41. package/src/dom/animations.ts +30 -31
  42. package/src/dom/highlight_overlay.ts +9 -5
  43. package/src/dom/ring_color.ts +85 -0
  44. package/src/index.ts +3 -0
  45. package/src/ui/history/conversation_history.ts +28 -4
  46. package/src/ui/placement/embedded_styles.ts +2 -2
  47. package/src/ui/placement/panel_placement.ts +34 -9
  48. package/src/ui/placement/preset_styles.ts +1 -1
  49. package/src/ui/placement/sidebar_styles.ts +1 -1
  50. package/src/ui/placement/small_viewport_styles.ts +5 -0
  51. package/src/ui/progress/tool_call_card.ts +18 -8
  52. package/src/ui/progress/tool_step_group.ts +3 -0
  53. package/src/ui/shell/token_styles.ts +2 -2
  54. 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";
@@ -354,8 +355,79 @@ var ClientToolRegistry = class {
354
355
  }
355
356
  };
356
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
+
357
430
  // src/dom/highlight_overlay.ts
358
- var ACCENT = "#4f46e5";
359
431
  function tokenOn(target, name, fallback) {
360
432
  const value = window.getComputedStyle(target).getPropertyValue(name).trim();
361
433
  return value === "" ? fallback : value;
@@ -440,7 +512,7 @@ function safeColor(value) {
440
512
  return CSS.supports("color", value) ? value : null;
441
513
  }
442
514
  function ringPaint(target, options) {
443
- const color = safeColor(options.color) ?? tokenOn(target, "--ag-ui-accent", ACCENT);
515
+ const color = safeColor(options.color) ?? ringColor(target, options.chat);
444
516
  if (options.gradient !== true) {
445
517
  return `border-color: ${color}`;
446
518
  }
@@ -504,9 +576,6 @@ function setNativeChecked(el2, checked) {
504
576
  }
505
577
 
506
578
  // src/dom/animations.ts
507
- var ACCENT2 = "#4f46e5";
508
- var ACCENT_HALO = "rgba(79, 70, 229, 0.4)";
509
- var ACCENT_PROPERTY = "--ag-ui-accent";
510
579
  function delay(ms) {
511
580
  return new Promise((resolve) => {
512
581
  setTimeout(resolve, ms);
@@ -521,12 +590,8 @@ function motionDelay(ms) {
521
590
  }
522
591
  return delay(ms);
523
592
  }
524
- function accentColor(el2, fallback) {
525
- const themed = window.getComputedStyle(el2).getPropertyValue(ACCENT_PROPERTY).trim();
526
- return themed === "" ? fallback : themed;
527
- }
528
- function ringShadow(el2) {
529
- 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)`;
530
595
  }
531
596
  async function typeInto(el2, value, options = {}) {
532
597
  const charDelayMs = options.charDelayMs ?? 35;
@@ -545,7 +610,7 @@ async function highlightThenClick(el2, options = {}) {
545
610
  const highlightMs = options.highlightMs ?? 280;
546
611
  const previousOutline = el2.style.outline;
547
612
  const previousOffset = el2.style.outlineOffset;
548
- el2.style.outline = `2px solid ${accentColor(el2, ACCENT2)}`;
613
+ el2.style.outline = `2px solid ${ringColor(el2, options.chat)}`;
549
614
  el2.style.outlineOffset = "2px";
550
615
  await delay(highlightMs);
551
616
  el2.style.outline = previousOutline;
@@ -597,7 +662,8 @@ async function flashRing(el2, options, focusByDefault) {
597
662
  scrim: options.scrim === true,
598
663
  gradient: options.gradient === true,
599
664
  ...options.color === void 0 ? {} : { color: options.color },
600
- ...options.ringPadding === void 0 ? {} : { padding: options.ringPadding }
665
+ ...options.ringPadding === void 0 ? {} : { padding: options.ringPadding },
666
+ ...options.chat === void 0 ? {} : { chat: options.chat }
601
667
  });
602
668
  await delay(flashMs);
603
669
  dismiss();
@@ -606,7 +672,7 @@ async function flashRing(el2, options, focusByDefault) {
606
672
  const previousOutline = el2.style.outline;
607
673
  const previousOffset = el2.style.outlineOffset;
608
674
  const previousTransition = el2.style.transition;
609
- const color = options.color ?? accentColor(el2, ACCENT2);
675
+ const color = options.color ?? ringColor(el2, options.chat);
610
676
  el2.style.outline = `3px solid ${color}`;
611
677
  el2.style.outlineOffset = "2px";
612
678
  const fadeMs = prefersReducedMotion() ? 0 : Math.round(flashMs * FLASH_FADE_RATIO);
@@ -633,7 +699,7 @@ async function pressThenClick(el2, options = {}) {
633
699
  const previousShadow = el2.style.boxShadow;
634
700
  el2.style.transition = "transform 80ms ease";
635
701
  el2.style.transform = "scale(0.96)";
636
- el2.style.boxShadow = ringShadow(el2);
702
+ el2.style.boxShadow = ringShadow(el2, options.chat);
637
703
  await motionDelay(pressMs);
638
704
  el2.style.transform = previousTransform;
639
705
  el2.style.transition = previousTransition;
@@ -656,7 +722,7 @@ async function selectOption(el2, value, options = {}) {
656
722
  const highlightMs = options.highlightMs ?? 220;
657
723
  const previousOutline = el2.style.outline;
658
724
  const previousOffset = el2.style.outlineOffset;
659
- el2.style.outline = `2px solid ${accentColor(el2, ACCENT2)}`;
725
+ el2.style.outline = `2px solid ${ringColor(el2, options.chat)}`;
660
726
  el2.style.outlineOffset = "2px";
661
727
  await motionDelay(highlightMs);
662
728
  setNativeValue(el2, option.value);
@@ -668,7 +734,7 @@ async function selectOption(el2, value, options = {}) {
668
734
  async function toggleControl(el2, checked, options = {}) {
669
735
  const flashMs = options.flashMs ?? 200;
670
736
  const previousShadow = el2.style.boxShadow;
671
- el2.style.boxShadow = ringShadow(el2);
737
+ el2.style.boxShadow = ringShadow(el2, options.chat);
672
738
  await motionDelay(flashMs);
673
739
  setNativeChecked(el2, checked);
674
740
  el2.dispatchEvent(new Event("input", { bubbles: true }));
@@ -4422,7 +4488,7 @@ var ConversationHistory = class {
4422
4488
  this.#host.conversationStore().setActiveThread(threadId);
4423
4489
  this.#threadId = threadId;
4424
4490
  this.#host.setRunning(false);
4425
- await this.rehydrate();
4491
+ await this.rehydrate("switch");
4426
4492
  }
4427
4493
  /** Rename a thread, then reload the list that shows its title. */
4428
4494
  renameThread(threadId, title) {
@@ -4440,6 +4506,7 @@ var ConversationHistory = class {
4440
4506
  this.#host.resetState();
4441
4507
  this.adoptActiveThread();
4442
4508
  this.#host.setRunning(false);
4509
+ this.#host.threadChanged(this.#threadId, "delete");
4443
4510
  }
4444
4511
  void this.refreshDrawer();
4445
4512
  }
@@ -4616,8 +4683,11 @@ var ConversationHistory = class {
4616
4683
  * for the run it would have resumed -- unless what replaced it is a restore
4617
4684
  * of the same conversation, still current, which answers for that run
4618
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.
4619
4689
  */
4620
- async rehydrate() {
4690
+ async rehydrate(reason) {
4621
4691
  this.#generation += 1;
4622
4692
  const generation = this.#generation;
4623
4693
  const threadId = this.#threadId;
@@ -4667,11 +4737,17 @@ var ConversationHistory = class {
4667
4737
  }
4668
4738
  }
4669
4739
  }
4740
+ if (checkpoint === null) {
4741
+ this.#noticeIfRunUnfinished(messages);
4742
+ }
4743
+ this.#host.threadChanged(threadId, reason);
4670
4744
  if (checkpoint !== null) {
4745
+ if (!this.#restoring(generation)) {
4746
+ this.#forgetStopped(store, threadId, checkpoint);
4747
+ return;
4748
+ }
4671
4749
  await this.#resumeFrom(checkpoint, generation);
4672
- return;
4673
4750
  }
4674
- this.#noticeIfRunUnfinished(messages);
4675
4751
  } finally {
4676
4752
  hold.release();
4677
4753
  if (this.#loading === waiting) {
@@ -5652,6 +5728,8 @@ function surfaceFor(placement) {
5652
5728
 
5653
5729
  // src/ui/placement/panel_placement.ts
5654
5730
  var SIZE_KEY = "ag-ui-chat:size";
5731
+ var DRAG_WIDTH = "--_drag-width";
5732
+ var DRAG_HEIGHT = "--_drag-height";
5655
5733
  var LAUNCHER_KEY = "ag-ui-chat:launcher";
5656
5734
  function asPoint(value) {
5657
5735
  if (typeof value !== "object" || value === null) {
@@ -5735,6 +5813,17 @@ var PanelPlacement = class {
5735
5813
  restoreSize() {
5736
5814
  this.#applySize(this.#readSize());
5737
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
+ }
5738
5827
  /**
5739
5828
  * A zero-sized box carrying the host's viewport insets as padding, so they
5740
5829
  * can be read back as used pixel lengths. See the `.viewport-probe` rule for
@@ -5923,12 +6012,12 @@ var PanelPlacement = class {
5923
6012
  */
5924
6013
  #measureAnchor() {
5925
6014
  const before = this.#host.element.getBoundingClientRect();
5926
- const width = this.#host.element.style.getPropertyValue("--ag-ui-width");
5927
- 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);
5928
6017
  const shrunk = this.#probeAnchor(before, -1);
5929
6018
  const grown = shrunk.x === null || shrunk.y === null ? this.#probeAnchor(before, 1) : shrunk;
5930
- this.#restoreProperty("--ag-ui-width", width);
5931
- this.#restoreProperty("--ag-ui-height", height);
6019
+ this.#restoreProperty(DRAG_WIDTH, width);
6020
+ this.#restoreProperty(DRAG_HEIGHT, height);
5932
6021
  return {
5933
6022
  // Neither direction moved it: the axis cannot be resized at all, so the
5934
6023
  // floating default is the best answer available and is the one the
@@ -5974,7 +6063,7 @@ var PanelPlacement = class {
5974
6063
  /**
5975
6064
  * Write a dragged size onto the host, on the axes this placement leaves free.
5976
6065
  *
5977
- * Writing the custom property rather than inline `width` / `height` does not
6066
+ * Writing a custom property rather than inline `width` / `height` does not
5978
6067
  * by itself leave placement in charge: an inline custom property still
5979
6068
  * outranks a `:host([placement=…])` rule setting the same property, so a
5980
6069
  * height dragged while floating would cap a docked sidebar asking for
@@ -5988,10 +6077,10 @@ var PanelPlacement = class {
5988
6077
  return;
5989
6078
  }
5990
6079
  if (size.width !== void 0) {
5991
- this.#host.element.style.setProperty("--ag-ui-width", `${size.width}px`);
6080
+ this.#host.element.style.setProperty(DRAG_WIDTH, `${size.width}px`);
5992
6081
  }
5993
6082
  if (size.height !== void 0 && axis === "both") {
5994
- this.#host.element.style.setProperty("--ag-ui-height", `${size.height}px`);
6083
+ this.#host.element.style.setProperty(DRAG_HEIGHT, `${size.height}px`);
5995
6084
  }
5996
6085
  }
5997
6086
  /**
@@ -6004,10 +6093,10 @@ var PanelPlacement = class {
6004
6093
  releaseOwnedAxes() {
6005
6094
  const axis = this.#resizeAxis();
6006
6095
  if (axis !== "both") {
6007
- this.#host.element.style.removeProperty("--ag-ui-height");
6096
+ this.#host.element.style.removeProperty(DRAG_HEIGHT);
6008
6097
  }
6009
6098
  if (axis === "none") {
6010
- this.#host.element.style.removeProperty("--ag-ui-width");
6099
+ this.#host.element.style.removeProperty(DRAG_WIDTH);
6011
6100
  }
6012
6101
  }
6013
6102
  /**
@@ -6910,9 +6999,7 @@ var ToolCallCard = class {
6910
6999
  this.#formatPayload = options.formatPayload ?? null;
6911
7000
  this.element = document.createElement("div");
6912
7001
  this.element.className = "tool-call";
6913
- this.element.setAttribute("part", "tool-card");
6914
7002
  this.element.setAttribute("data-tool-name", name);
6915
- this.element.setAttribute("data-status", TOOL_CALL_STATUS.PENDING);
6916
7003
  this.element.setAttribute("data-expanded", "false");
6917
7004
  const head = document.createElement("div");
6918
7005
  head.className = "tool-call-head";
@@ -6927,8 +7014,7 @@ var ToolCallCard = class {
6927
7014
  label.textContent = summary ?? name;
6928
7015
  this.#status = document.createElement("span");
6929
7016
  this.#status.className = "tool-call-status";
6930
- this.#status.setAttribute("part", "tool-card-status");
6931
- this.#status.textContent = statusLabels(strings)[TOOL_CALL_STATUS.PENDING];
7017
+ this.#setStatus(TOOL_CALL_STATUS.PENDING);
6932
7018
  this.#decision = document.createElement("span");
6933
7019
  this.#decision.className = "tool-call-decision";
6934
7020
  this.#decision.setAttribute("part", "tool-card-decision");
@@ -6976,8 +7062,7 @@ var ToolCallCard = class {
6976
7062
  if (this.#settled) {
6977
7063
  return;
6978
7064
  }
6979
- this.element.setAttribute("data-status", status);
6980
- this.#status.textContent = statusLabels(this.#strings)[status];
7065
+ this.#setStatus(status);
6981
7066
  }
6982
7067
  /**
6983
7068
  * Say whether a prompt is open on this call, a question it asked or a
@@ -7012,8 +7097,7 @@ var ToolCallCard = class {
7012
7097
  return;
7013
7098
  }
7014
7099
  this.#settled = true;
7015
- this.element.setAttribute("data-status", status);
7016
- this.#status.textContent = statusLabels(this.#strings)[status];
7100
+ this.#setStatus(status);
7017
7101
  this.#resultLabel.textContent = resultLabels(this.#strings)[status];
7018
7102
  this.#renderPayload(
7019
7103
  this.#resultBody,
@@ -7022,6 +7106,20 @@ var ToolCallCard = class {
7022
7106
  );
7023
7107
  this.#resultSection.hidden = false;
7024
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
+ }
7025
7123
  /**
7026
7124
  * Fill one body region, offering it to the host formatter first.
7027
7125
  *
@@ -8373,8 +8471,8 @@ var EMBEDDED_STYLES = `
8373
8471
  the widget lives in the host's own layout (fixes overlay/z-index clashes). */
8374
8472
  :host([placement="embedded"]) {
8375
8473
  --_position: var(--ag-ui-position, static);
8376
- --_width: var(--ag-ui-width, 100%);
8377
- --_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%));
8378
8476
  --_max-width: var(--ag-ui-max-width, 100%);
8379
8477
  --_max-height: var(--ag-ui-max-height, 100%);
8380
8478
  --_shadow: var(--ag-ui-shadow, none);
@@ -8632,7 +8730,7 @@ var PRESET_STYLES = `
8632
8730
 
8633
8731
  :host([placement="side"]) {
8634
8732
  --_inset: var(--ag-ui-inset, calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right) var(--_viewport-inset-bottom) auto);
8635
- --_width: var(--ag-ui-width, 420px);
8733
+ --_width: var(--_drag-width, var(--ag-ui-width, 420px));
8636
8734
  --_height: var(--ag-ui-height, var(--_viewport-height));
8637
8735
  --_max-height: var(--ag-ui-max-height, var(--_viewport-height));
8638
8736
  --_panel-radius: var(--ag-ui-panel-radius, 0);
@@ -8848,7 +8946,7 @@ var SIDEBAR_STYLES = `
8848
8946
  host-managed push instead. */
8849
8947
  :host([placement="sidebar"]) {
8850
8948
  --_inset: var(--ag-ui-inset, calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right) var(--_viewport-inset-bottom) auto);
8851
- --_width: var(--ag-ui-width, 420px);
8949
+ --_width: var(--_drag-width, var(--ag-ui-width, 420px));
8852
8950
  --_height: var(--ag-ui-height, var(--_viewport-height));
8853
8951
  --_max-height: var(--ag-ui-max-height, var(--_viewport-height));
8854
8952
  --_panel-radius: var(--ag-ui-panel-radius, 0);
@@ -8947,6 +9045,11 @@ var SMALL_VIEWPORT_STYLES = `
8947
9045
  fine-pointered and small, and conflating the two gets both wrong. What the
8948
9046
  pointer decides is which controls make sense, further down.
8949
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
+
8950
9053
  Two placements are left alone. "page" is already this shape. "embedded" sits
8951
9054
  in a box the host sized and placed, and taking that over would break the app
8952
9055
  shell it was embedded into -- the host is the only party that knows whether
@@ -10227,8 +10330,8 @@ var TOKEN_STYLES = `
10227
10330
  grid/flex layout) to embed it in the page flow instead of floating. */
10228
10331
  --_position: var(--ag-ui-position, fixed);
10229
10332
  --_z-index: var(--ag-ui-z-index, 2147483000);
10230
- --_width: var(--ag-ui-width, 380px);
10231
- --_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));
10232
10335
  /* The gutter a resting floating panel keeps between itself and the edge of
10233
10336
  the box the host left free. One number, because the inset below spends it
10234
10337
  and the cap beneath has to know it was spent. */
@@ -16292,6 +16395,7 @@ var ToolStepGroup = class {
16292
16395
  state = "error";
16293
16396
  }
16294
16397
  this.element.setAttribute("data-state", state);
16398
+ this.element.setAttribute("part", `tool-group tool-group-${state}`);
16295
16399
  this.element.setAttribute("data-count", String(total));
16296
16400
  const only = total === 1 ? steps[0] : void 0;
16297
16401
  if (only !== void 0) {
@@ -17998,8 +18102,13 @@ var RunHandlers = class {
17998
18102
  * A handler table for one AG-UI client. Every table built for the current
17999
18103
  * conversation draws into the same transcript and shares its bookkeeping, so
18000
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.
18001
18110
  */
18002
- forClient() {
18111
+ forClient(threadId) {
18003
18112
  const ledger = this.#ledger;
18004
18113
  const attached = () => ledger === this.#ledger;
18005
18114
  const { onSettled, ...drawing } = this.#tableFor(ledger);
@@ -18009,7 +18118,7 @@ var RunHandlers = class {
18009
18118
  if (attached()) {
18010
18119
  onSettled();
18011
18120
  }
18012
- this.#dispatchRunFinished(ledger);
18121
+ this.#dispatchRunFinished(ledger, threadId);
18013
18122
  }
18014
18123
  };
18015
18124
  }
@@ -18162,7 +18271,7 @@ var RunHandlers = class {
18162
18271
  * server is a fact about the run, and a name can appear on both sides across a
18163
18272
  * conversation.
18164
18273
  */
18165
- #dispatchRunFinished(ledger) {
18274
+ #dispatchRunFinished(ledger, threadId) {
18166
18275
  const tools = ledger.tools.map(({ id, name }) => ({
18167
18276
  name,
18168
18277
  side: ledger.serverSettled.has(id) ? "server" : "client"
@@ -18173,7 +18282,7 @@ var RunHandlers = class {
18173
18282
  ledger.invalidated.clear();
18174
18283
  this.#host.element.dispatchEvent(
18175
18284
  new CustomEvent(RUN_FINISHED_EVENT, {
18176
- detail: { tools, invalidated },
18285
+ detail: { threadId, tools, invalidated },
18177
18286
  bubbles: true,
18178
18287
  composed: true
18179
18288
  })
@@ -18992,6 +19101,11 @@ var AgUiChat = class extends HTMLElement {
18992
19101
  * then a host's {@link sendMessage} waits for it rather than refusing.
18993
19102
  */
18994
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;
18995
19109
  /** The decision a run is suspended on, which a Stop abandons. */
18996
19110
  #decision = new PendingDecision();
18997
19111
  /**
@@ -19023,6 +19137,8 @@ var AgUiChat = class extends HTMLElement {
19023
19137
  allowImages: () => this.allowImages,
19024
19138
  follow: () => this.#transcript.follow()
19025
19139
  });
19140
+ // This chat's place among the connected ones, which a free highlight ring reads.
19141
+ #chatRegistration = new ChatRegistration(this);
19026
19142
  /**
19027
19143
  * Which storage keys are this element's: the namespace it claims, the keys
19028
19144
  * its layout preferences live under, and the built-in store scoped to them.
@@ -19270,7 +19386,8 @@ var AgUiChat = class extends HTMLElement {
19270
19386
  },
19271
19387
  cancelRun: () => this.#cancelRun(),
19272
19388
  resetState: () => this.#resetState(),
19273
- setRunning: (running) => this.#setRunning(running)
19389
+ setRunning: (running) => this.#setRunning(running),
19390
+ threadChanged: (threadId, reason) => this.#dispatchThread(threadId, reason)
19274
19391
  });
19275
19392
  }
19276
19393
  /** Attributes the element reacts to after it has been connected. */
@@ -19583,6 +19700,7 @@ var AgUiChat = class extends HTMLElement {
19583
19700
  }
19584
19701
  this.#connectedBefore = true;
19585
19702
  this.#storage.claim();
19703
+ this.#chatRegistration.join();
19586
19704
  this.#placement.restoreSize();
19587
19705
  requestAnimationFrame(() => {
19588
19706
  this.#placement.restoreLauncherPosition();
@@ -19620,7 +19738,7 @@ var AgUiChat = class extends HTMLElement {
19620
19738
  this.#voice.wire();
19621
19739
  this.#history.adoptActiveThread();
19622
19740
  queueMicrotask(() => this.#startup());
19623
- void this.#history.rehydrate();
19741
+ void this.#history.rehydrate(changed && held !== "" ? "rescope" : "restore");
19624
19742
  if (changed) {
19625
19743
  void this.#history.refreshDrawer();
19626
19744
  }
@@ -19673,7 +19791,7 @@ var AgUiChat = class extends HTMLElement {
19673
19791
  await Promise.all([
19674
19792
  this.#tools.fetchCatalog(),
19675
19793
  this.#skills.fetch(),
19676
- this.#history.rehydrate()
19794
+ this.#history.rehydrate("restore")
19677
19795
  ]);
19678
19796
  }
19679
19797
  /**
@@ -19689,6 +19807,7 @@ var AgUiChat = class extends HTMLElement {
19689
19807
  window.visualViewport?.removeEventListener("resize", this.#onViewportResize);
19690
19808
  window.visualViewport?.removeEventListener("scroll", this.#onViewportResize);
19691
19809
  this.#storage.release();
19810
+ this.#chatRegistration.leave();
19692
19811
  this.#cancelRun();
19693
19812
  this.#excerpts.detachPageOffer();
19694
19813
  this.#attachments.tray?.dispose();
@@ -19836,6 +19955,26 @@ var AgUiChat = class extends HTMLElement {
19836
19955
  get unread() {
19837
19956
  return this.#unread;
19838
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
+ }
19839
19978
  /**
19840
19979
  * Whether to mount collapsed.
19841
19980
  *
@@ -19886,6 +20025,13 @@ var AgUiChat = class extends HTMLElement {
19886
20025
  moveTo(corner, options = {}) {
19887
20026
  return this.#placement.moveTo(corner, options);
19888
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
+ }
19889
20035
  /** Flip the collapsed state. Bound to the built-in header toggle. */
19890
20036
  toggleCollapsed() {
19891
20037
  this.setCollapsed(!this.collapsed);
@@ -19955,7 +20101,7 @@ var AgUiChat = class extends HTMLElement {
19955
20101
  this.#voice.dispose();
19956
20102
  this.#voice.wire();
19957
20103
  this.#history.adoptActiveThread();
19958
- void this.#history.rehydrate();
20104
+ void this.#history.rehydrate("rescope");
19959
20105
  void this.#history.refreshDrawer();
19960
20106
  }
19961
20107
  /**
@@ -20016,6 +20162,34 @@ var AgUiChat = class extends HTMLElement {
20016
20162
  closeCheckpoints() {
20017
20163
  this.#checkpoints.close();
20018
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
+ }
20019
20193
  /**
20020
20194
  * Open the checkpoints panel, or close it if it is already open — what the
20021
20195
  * built-in ⭯ button does, because a control that opens a panel is read as the
@@ -20048,6 +20222,7 @@ var AgUiChat = class extends HTMLElement {
20048
20222
  this.#history.startThread();
20049
20223
  this.#setRunning(false);
20050
20224
  this.#setUnread(0);
20225
+ this.#dispatchThread(this.#history.threadId, "new");
20051
20226
  if (!this.collapsed) {
20052
20227
  this.#input.focus({ preventScroll: true });
20053
20228
  }
@@ -20240,6 +20415,8 @@ var AgUiChat = class extends HTMLElement {
20240
20415
  },
20241
20416
  onFilesPicked: () => this.#attachments.onFilesPicked()
20242
20417
  });
20418
+ const aboveComposer = document.createElement("slot");
20419
+ aboveComposer.name = "above-composer";
20243
20420
  const footer = document.createElement("slot");
20244
20421
  footer.name = "footer";
20245
20422
  this.#messagesWrap.className = "messages-wrap";
@@ -20252,6 +20429,7 @@ var AgUiChat = class extends HTMLElement {
20252
20429
  this.#composerHint,
20253
20430
  this.#queuedRow,
20254
20431
  this.#attachSlot,
20432
+ aboveComposer,
20255
20433
  inputRow,
20256
20434
  footer,
20257
20435
  this.#drawer.element,
@@ -20419,6 +20597,21 @@ var AgUiChat = class extends HTMLElement {
20419
20597
  this.#sending = null;
20420
20598
  this.#history.stopContinuation();
20421
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
+ }
20422
20615
  /**
20423
20616
  * Swap the composer button between Send (idle) and Stop (running).
20424
20617
  *
@@ -20436,6 +20629,7 @@ var AgUiChat = class extends HTMLElement {
20436
20629
  this.#send.title = label;
20437
20630
  this.#send.setAttribute("aria-label", label);
20438
20631
  this.#send.dataset["state"] = running ? "running" : "idle";
20632
+ this.toggleAttribute("data-running", running);
20439
20633
  if (settled) {
20440
20634
  this.#flushQueued();
20441
20635
  }
@@ -20724,7 +20918,7 @@ var AgUiChat = class extends HTMLElement {
20724
20918
  });
20725
20919
  return new AgUiClient({
20726
20920
  agent,
20727
- handlers: this.#runHandlers.forClient(),
20921
+ handlers: this.#runHandlers.forClient(threadId),
20728
20922
  getTools: () => this.#tools.advertise(),
20729
20923
  getContext: () => this.#dispatch.buildContext(),
20730
20924
  executeTool: (call) => this.#dispatch.execute(call),
@@ -20918,7 +21112,7 @@ function setControlValue(el2, value) {
20918
21112
  }
20919
21113
 
20920
21114
  // src/version.ts
20921
- var VERSION = "0.46.0";
21115
+ var VERSION = "0.47.0";
20922
21116
  export {
20923
21117
  ATTACHMENT_EVENT,
20924
21118
  AgUiChat,
@@ -20951,6 +21145,7 @@ export {
20951
21145
  SUBMIT_EVENT,
20952
21146
  SUGGESTIONS_ACTIVITY_TYPE,
20953
21147
  SessionStorageStore,
21148
+ THREAD_EVENT,
20954
21149
  TOGGLE_EVENT,
20955
21150
  TOOL_CALL_STATUS,
20956
21151
  TOOL_DISPLAY,