@artooi/ag-ui-web-component 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  var ELEMENT_TAG = "ag-ui-chat";
3
3
  var SUBMIT_EVENT = "ag-ui-submit";
4
4
  var TOGGLE_EVENT = "ag-ui-toggle";
5
+ var UNREAD_EVENT = "ag-ui-unread";
5
6
  var STATE_EVENT = "ag-ui-state";
6
7
  var ATTACHMENT_EVENT = "ag-ui-attachments";
7
8
  var MESSAGE_ROLE = {
@@ -34,6 +35,11 @@ var TOOL_DISPLAY = {
34
35
  };
35
36
  var COMPACTION_ACTIVITY_TYPE = "compaction";
36
37
  var LOAD_CAPABILITY_TOOL = "load_capability";
38
+ var ICON_SEND = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 19.5V5m-6.5 6.5L12 5l6.5 6.5"/></svg>`;
39
+ var ICON_STOP = `<svg class="glyph glyph--solid" viewBox="0 0 24 24" aria-hidden="true"><rect x="7" y="7" width="10" height="10" rx="2.5"/></svg>`;
40
+ var ICON_ATTACH = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M17 8.5V15a5 5 0 0 1-10 0V7a3 3 0 0 1 6 0v7.5a1 1 0 0 1-2 0V8.5"/></svg>`;
41
+ var ICON_VOICE = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4a3 3 0 0 1 3 3v5a3 3 0 0 1-6 0V7a3 3 0 0 1 3-3z"/><path d="M5 11v1a7 7 0 0 0 14 0v-1"/><path d="M12 19v3"/></svg>`;
42
+ var ICON_LAUNCHER = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 5.5A2.5 2.5 0 0 1 6.5 3h11A2.5 2.5 0 0 1 20 5.5v8a2.5 2.5 0 0 1-2.5 2.5H9l-5 4z"/></svg>`;
37
43
 
38
44
  // src/skills/fill_template.ts
39
45
  var PLACEHOLDER_RE = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
@@ -509,6 +515,7 @@ var DEFAULT_UI_STRINGS = {
509
515
  newChat: "New chat",
510
516
  collapse: "Collapse",
511
517
  expand: "Expand",
518
+ expandUnread: "Expand \u2014 {count} unread",
512
519
  toggleTheme: "Toggle theme",
513
520
  copyCode: "Copy",
514
521
  copied: "Copied",
@@ -4532,6 +4539,36 @@ var STYLES = `
4532
4539
  --_icon-size: var(--ag-ui-icon-size, 22px);
4533
4540
  --_icon-radius: var(--ag-ui-icon-radius, 4px);
4534
4541
 
4542
+ /* Composer \u2014 one bordered surface holding the field and its tool row. */
4543
+ --_composer-radius: var(--ag-ui-composer-radius, 14px);
4544
+ --_composer-max-height: var(--ag-ui-composer-max-height, 40vh);
4545
+ --_tool-btn-size: var(--ag-ui-tool-btn-size, 30px);
4546
+ --_send-size: var(--ag-ui-send-size, 30px);
4547
+ --_glyph-size: var(--ag-ui-glyph-size, 18px);
4548
+ --_glyph-stroke: var(--ag-ui-glyph-stroke, 1.75);
4549
+
4550
+ /* Unread badge on the launcher. */
4551
+ --_badge-bg: var(--ag-ui-badge-bg, var(--_danger));
4552
+ --_badge-fg: var(--ag-ui-badge-fg, #ffffff);
4553
+ --_badge-size: var(--ag-ui-badge-size, 18px);
4554
+ --_badge-font-size: var(--ag-ui-badge-font-size, 11px);
4555
+
4556
+ /* The floating launcher a collapsed widget shrinks to. */
4557
+ --_launcher-size: var(--ag-ui-launcher-size, 56px);
4558
+ --_launcher-bg: var(--ag-ui-launcher-bg, var(--_header-bg));
4559
+ --_launcher-fg: var(--ag-ui-launcher-fg, var(--_header-fg));
4560
+ --_launcher-radius: var(--ag-ui-launcher-radius, 50%);
4561
+ --_launcher-icon-size: var(--ag-ui-launcher-icon-size, 26px);
4562
+ --_launcher-inset: var(--ag-ui-launcher-inset, auto 0 0 auto);
4563
+
4564
+ /* Motion. One duration and two curves drive every collapse, expand and
4565
+ slide-over, so the whole widget accelerates and settles as one thing.
4566
+ The default curve is a long-tailed ease-out (a fast start that decelerates
4567
+ into place); the pop curve overshoots slightly, for something arriving. */
4568
+ --_motion: var(--ag-ui-motion, 0.28s);
4569
+ --_ease: var(--ag-ui-ease, cubic-bezier(0.32, 0.72, 0, 1));
4570
+ --_ease-pop: var(--ag-ui-ease-pop, cubic-bezier(0.34, 1.36, 0.64, 1));
4571
+
4535
4572
  /* Spacing \u2014 the density preset overrides these. */
4536
4573
  --_space: var(--ag-ui-space, 10px);
4537
4574
  --_pad: var(--ag-ui-pad, 16px);
@@ -4696,56 +4733,161 @@ var STYLES = `
4696
4733
  --_height: var(--ag-ui-height, 100vh);
4697
4734
  --_max-height: var(--ag-ui-max-height, 100vh);
4698
4735
  --_radius: var(--ag-ui-radius, 0);
4699
- transition: width 0.28s ease;
4736
+ transition: width var(--_motion) var(--_ease);
4700
4737
  }
4701
4738
 
4702
4739
  :host([placement="sidebar"][data-side="left"]) {
4703
4740
  --_inset: var(--ag-ui-inset, 0 auto 0 0);
4704
4741
  }
4705
4742
 
4743
+ /* The docked panel is pinned to the edge it docks against rather than filling
4744
+ the host as a flex child. Collapsing shrinks the host to the rail width, and
4745
+ a flex child would be squashed to 52px on the way out instead of sliding out
4746
+ at full width. */
4706
4747
  :host([placement="sidebar"]) .chat {
4707
- transition: transform 0.28s ease;
4748
+ position: absolute;
4749
+ inset: 0 0 0 auto;
4750
+ width: var(--_width);
4751
+ transform-origin: right center;
4708
4752
  }
4709
4753
 
4710
- /* Collapsed sidebar: shrink the host to the rail width, hide the panel, and
4711
- reveal the rail. Higher specificity than the generic collapse rules, so it
4712
- wins regardless of source order. */
4754
+ :host([placement="sidebar"][data-side="left"]) .chat {
4755
+ inset: 0 auto 0 0;
4756
+ transform-origin: left center;
4757
+ }
4758
+
4759
+ /* Collapsed sidebar: shrink the host to the rail width and slide the panel out
4760
+ through the edge it docks against. Higher specificity than the generic
4761
+ collapse rules, so it wins regardless of source order. */
4713
4762
  :host([placement="sidebar"][collapsed]) {
4714
4763
  width: var(--_rail-width);
4715
4764
  height: 100vh;
4716
4765
  max-height: 100vh;
4717
4766
  bottom: 0;
4767
+ pointer-events: auto;
4718
4768
  }
4719
4769
 
4720
4770
  :host([placement="sidebar"][collapsed]) .chat {
4721
- display: none;
4771
+ transform: translateX(100%);
4722
4772
  }
4723
4773
 
4724
- /* The rail is a sibling of the panel (so it survives the panel being hidden);
4725
- shown only for a collapsed sidebar. */
4726
- .rail {
4727
- display: none;
4774
+ :host([placement="sidebar"][data-side="left"][collapsed]) .chat {
4775
+ transform: translateX(-100%);
4776
+ }
4777
+
4778
+ @media (prefers-reduced-motion: reduce) {
4779
+ :host([placement="sidebar"]),
4780
+ :host([placement="sidebar"]) .chat {
4781
+ transition: none;
4782
+ }
4783
+ }
4784
+
4785
+ /* \u2500\u2500 Launcher \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\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
4786
+ What a collapsed widget shrinks to: a round floating button by default, the
4787
+ sidebar's slim edge rail under that placement. A sibling of the panel, so it
4788
+ survives the panel being hidden, and the only part of a collapsed widget
4789
+ that takes pointer events. */
4790
+ .launcher {
4791
+ display: flex;
4792
+ position: absolute;
4793
+ inset: var(--_launcher-inset);
4794
+ align-items: center;
4795
+ justify-content: center;
4796
+ width: var(--_launcher-size);
4797
+ height: var(--_launcher-size);
4798
+ padding: 0;
4728
4799
  border: none;
4800
+ border-radius: var(--_launcher-radius);
4801
+ background: var(--_launcher-bg);
4802
+ color: var(--_launcher-fg);
4803
+ box-shadow: var(--_shadow);
4729
4804
  font: inherit;
4805
+ cursor: pointer;
4806
+ pointer-events: auto;
4807
+ opacity: 0;
4808
+ transform: scale(0.4);
4809
+ visibility: hidden;
4810
+ transition:
4811
+ opacity var(--_motion) var(--_ease),
4812
+ transform var(--_motion) var(--_ease-pop),
4813
+ visibility var(--_motion) var(--_ease);
4814
+ }
4815
+
4816
+ /* The launcher grows out of the corner the panel shrank into.
4817
+ It is laid out at rest rather than display:none, which is what lets it
4818
+ animate in *and* out: an element that was not rendered has no before-change
4819
+ style to transition from, and one whose display flips to none cannot
4820
+ transition at all. visibility keeps it unpaintable, untabbable and
4821
+ unclickable in between -- the expanded panel's own controls sit under it and
4822
+ must stay reachable. */
4823
+ :host([collapsed]) .launcher {
4824
+ opacity: 1;
4825
+ transform: none;
4826
+ visibility: visible;
4730
4827
  }
4731
4828
 
4732
- :host([placement="sidebar"][collapsed]) .rail {
4733
- display: flex;
4829
+ :host([collapsed]) .launcher:hover {
4830
+ transform: scale(1.06);
4831
+ }
4832
+
4833
+ :host([collapsed]) .launcher:active {
4834
+ transform: scale(0.94);
4835
+ }
4836
+
4837
+ .launcher .icon-holder {
4838
+ width: var(--_launcher-icon-size);
4839
+ height: var(--_launcher-icon-size);
4840
+ }
4841
+
4842
+ /* The unread badge rides the launcher's top-right corner. The ring in the
4843
+ widget's own background is what separates it from the launcher underneath,
4844
+ whatever colour the host themes either one. */
4845
+ .launcher-badge {
4734
4846
  position: absolute;
4847
+ top: -2px;
4848
+ right: -2px;
4849
+ display: inline-flex;
4850
+ align-items: center;
4851
+ justify-content: center;
4852
+ box-sizing: border-box;
4853
+ min-width: var(--_badge-size);
4854
+ height: var(--_badge-size);
4855
+ padding: 0 5px;
4856
+ border-radius: 999px;
4857
+ background: var(--_badge-bg);
4858
+ color: var(--_badge-fg);
4859
+ font-size: var(--_badge-font-size);
4860
+ font-weight: 600;
4861
+ line-height: 1;
4862
+ box-shadow: 0 0 0 2px var(--_bg);
4863
+ }
4864
+
4865
+ .launcher-badge[hidden] {
4866
+ display: none;
4867
+ }
4868
+
4869
+ /* The sidebar collapses to an edge rail instead: full height, square, flush
4870
+ against the dock. It slides in with the panel rather than popping. */
4871
+ :host([placement="sidebar"][collapsed]) .launcher {
4735
4872
  inset: 0;
4873
+ width: auto;
4874
+ height: auto;
4736
4875
  align-items: flex-start;
4737
- justify-content: center;
4738
4876
  padding-top: 16px;
4739
4877
  border: 1px solid var(--_border);
4878
+ border-radius: 0;
4740
4879
  background: var(--_header-bg);
4741
4880
  color: var(--_header-fg);
4742
- cursor: pointer;
4881
+ box-shadow: none;
4882
+ transform: none;
4743
4883
  }
4744
4884
 
4885
+ /* Every transition in this file is timed by --_motion, so collapsing it to a
4886
+ frame is the whole reduced-motion story: states still change (and display
4887
+ still flips at the end of its discrete transition), nothing travels. */
4745
4888
  @media (prefers-reduced-motion: reduce) {
4746
- :host([placement="sidebar"]),
4747
- :host([placement="sidebar"]) .chat {
4748
- transition: none;
4889
+ :host {
4890
+ --_motion: var(--ag-ui-motion, 0.001s);
4749
4891
  }
4750
4892
  }
4751
4893
 
@@ -4836,27 +4978,78 @@ var STYLES = `
4836
4978
  background: rgba(255, 255, 255, 0.18);
4837
4979
  }
4838
4980
 
4839
- /* Collapsed: keep just the header bar \u2014 hide the whole body, and let the host
4840
- shrink to the header. (A host can also fully hide the element itself.) */
4981
+ /* \u2500\u2500 Collapse \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\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
4982
+ Collapsing shrinks the widget to the round floating launcher: the panel
4983
+ scales down toward the launcher's corner and fades out, the launcher pops in
4984
+ from that same point. Both halves are transform and opacity only, so the
4985
+ morph runs on the compositor and never reflows the host page.
4986
+
4987
+ The host box keeps its expanded size. Animating it would animate layout, and
4988
+ a dragged --ag-ui-width would then fight the launcher's own size. Nothing
4989
+ paints there once the panel is gone, so the box only has to stop swallowing
4990
+ clicks: pointer events go to none, and the launcher takes them back.
4991
+
4992
+ Two placements collapse to something else and restore what they need:
4993
+ "sidebar" slides to its rail (below), while "embedded" and "page" keep the
4994
+ original header bar \u2014 embedded is laid out by the host page, where a
4995
+ floating circle would escape the layout, and page is a full-screen route
4996
+ with no corner to float in. */
4841
4997
  :host([collapsed]) {
4998
+ pointer-events: none;
4999
+ }
5000
+
5001
+ :host([collapsed]) .chat {
5002
+ opacity: 0;
5003
+ transform: scale(0.94);
5004
+ visibility: hidden;
5005
+ }
5006
+
5007
+ /* visibility is what keeps the panel out of the tab order and the a11y tree at
5008
+ rest without display:none killing the transition. It interpolates so that any
5009
+ progress below 1 still counts as visible: the panel stays on screen for the
5010
+ whole collapse and flips hidden exactly at the end, and on expand it is
5011
+ visible from the first frame. */
5012
+ .chat {
5013
+ transform-origin: bottom right;
5014
+ transition:
5015
+ opacity var(--_motion) var(--_ease),
5016
+ transform var(--_motion) var(--_ease),
5017
+ visibility var(--_motion) var(--_ease);
5018
+ }
5019
+
5020
+ :host([placement="bottom-left"]) .chat {
5021
+ transform-origin: bottom left;
5022
+ }
5023
+
5024
+ /* The two in-flow placements keep the original collapse: hide the body, let
5025
+ the host shrink to the header bar. */
5026
+ :host([collapsed]:is([placement="embedded"], [placement="page"])) {
4842
5027
  height: auto;
4843
5028
  max-height: none;
5029
+ pointer-events: auto;
4844
5030
  }
4845
5031
 
4846
- :host([collapsed]) .messages,
4847
- :host([collapsed]) .input-row,
4848
- :host([collapsed]) .skill-chips,
4849
- :host([collapsed]) .skill-palette,
4850
- :host([collapsed]) .skill-hint {
4851
- display: none;
5032
+ :host([collapsed]:is([placement="embedded"], [placement="page"])) .chat {
5033
+ opacity: 1;
5034
+ transform: none;
5035
+ visibility: visible;
4852
5036
  }
4853
5037
 
4854
- /* Edge-docked placements pin top *and* bottom, which would otherwise keep the
4855
- panel full-height when collapsed; unpin the bottom so it shrinks to the
4856
- header. (Floating/bottom-left pin only the bottom, so they already shrink.) */
4857
- :host([collapsed][placement="side"]),
4858
- :host([collapsed][placement="full"]) {
4859
- bottom: auto;
5038
+ /* These two keep the header bar, so the launcher must stay out of the way \u2014
5039
+ an embedded host is position: static, which would otherwise leave an
5040
+ absolutely-positioned circle to escape the layout entirely and land against
5041
+ whatever the page happens to position. */
5042
+ :host([collapsed]:is([placement="embedded"], [placement="page"])) .launcher {
5043
+ visibility: hidden;
5044
+ opacity: 0;
5045
+ }
5046
+
5047
+ :host([collapsed]:is([placement="embedded"], [placement="page"])) .messages,
5048
+ :host([collapsed]:is([placement="embedded"], [placement="page"])) .input-row,
5049
+ :host([collapsed]:is([placement="embedded"], [placement="page"])) .skill-chips,
5050
+ :host([collapsed]:is([placement="embedded"], [placement="page"])) .skill-palette,
5051
+ :host([collapsed]:is([placement="embedded"], [placement="page"])) .skill-hint {
5052
+ display: none;
4860
5053
  }
4861
5054
 
4862
5055
  .messages {
@@ -5529,102 +5722,187 @@ var STYLES = `
5529
5722
  opacity: 0.35;
5530
5723
  }
5531
5724
 
5725
+ /* \u2500\u2500 Composer \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\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
5726
+ One surface owns the border, the background and the focus ring; the field and
5727
+ its tool row sit inside it. The row used to be four siblings stretched to the
5728
+ textarea's height, which gave a paperclip the same visual weight as the field
5729
+ and turned Send into a full-height slab. */
5532
5730
  .input-row {
5533
5731
  display: flex;
5534
- gap: 8px;
5535
5732
  padding: 12px;
5536
5733
  border-top: 1px solid var(--_border);
5537
5734
  }
5538
5735
 
5539
- .input {
5736
+ .composer {
5540
5737
  flex: 1;
5541
- resize: none;
5738
+ min-width: 0;
5739
+ display: flex;
5740
+ flex-direction: column;
5741
+ gap: 4px;
5742
+ padding: 6px 6px 6px 10px;
5542
5743
  background: var(--_input-bg);
5543
5744
  border: 1px solid var(--_border);
5544
- border-radius: 8px;
5545
- padding: 8px 10px;
5745
+ border-radius: var(--_composer-radius);
5746
+ transition: border-color var(--_motion) var(--_ease);
5747
+ }
5748
+
5749
+ .composer:focus-within {
5750
+ border-color: var(--_accent);
5751
+ }
5752
+
5753
+ /* The field grows with its content (sized by #autoGrow) up to the ceiling,
5754
+ then scrolls. border-box keeps that measurement stable: with content-box the
5755
+ padding would be added to every scrollHeight read and the field would creep
5756
+ taller on each keystroke. */
5757
+ .input {
5758
+ box-sizing: border-box;
5759
+ resize: none;
5760
+ max-height: var(--_composer-max-height);
5761
+ overflow-y: auto;
5762
+ padding: 6px 4px 2px;
5763
+ background: transparent;
5764
+ border: none;
5546
5765
  font: inherit;
5547
5766
  color: inherit;
5548
5767
  outline: none;
5549
5768
  }
5550
5769
 
5551
- .input:focus {
5552
- border-color: var(--_accent);
5770
+ .composer-tools {
5771
+ display: flex;
5772
+ align-items: center;
5773
+ gap: 2px;
5553
5774
  }
5554
5775
 
5555
- .send {
5776
+ /* Icon buttons: quiet at rest, so the field is what the eye lands on. */
5777
+ .attach-btn,
5778
+ .voice-btn {
5779
+ display: inline-flex;
5780
+ align-items: center;
5781
+ justify-content: center;
5782
+ width: var(--_tool-btn-size);
5783
+ height: var(--_tool-btn-size);
5784
+ padding: 0;
5556
5785
  border: none;
5557
5786
  border-radius: 8px;
5558
- padding: 0 16px;
5787
+ background: transparent;
5788
+ color: var(--_muted);
5789
+ font: inherit;
5790
+ line-height: 1;
5791
+ cursor: pointer;
5792
+ transition:
5793
+ background-color var(--_motion) var(--_ease),
5794
+ color var(--_motion) var(--_ease);
5795
+ }
5796
+
5797
+ .attach-btn:hover,
5798
+ .voice-btn:hover {
5799
+ background: var(--_hover);
5800
+ color: var(--_fg);
5801
+ }
5802
+
5803
+ .attach-btn:disabled,
5804
+ .voice-btn:disabled {
5805
+ cursor: default;
5806
+ opacity: 0.6;
5807
+ }
5808
+
5809
+ /* Send closes the row on the right: a circle, the only filled control in the
5810
+ composer, so "the thing that acts" reads at a glance. */
5811
+ .send {
5812
+ margin-left: auto;
5813
+ display: inline-flex;
5814
+ align-items: center;
5815
+ justify-content: center;
5816
+ flex: none;
5817
+ width: var(--_send-size);
5818
+ height: var(--_send-size);
5819
+ padding: 0;
5820
+ border: none;
5821
+ border-radius: 50%;
5559
5822
  background: var(--_accent);
5560
5823
  color: #ffffff;
5561
5824
  font: inherit;
5562
- font-weight: 600;
5825
+ line-height: 1;
5563
5826
  cursor: pointer;
5827
+ transition:
5828
+ background-color var(--_motion) var(--_ease),
5829
+ transform var(--_motion) var(--_ease-pop);
5830
+ }
5831
+
5832
+ .send:hover {
5833
+ transform: scale(1.08);
5834
+ }
5835
+
5836
+ .send:active {
5837
+ transform: scale(0.92);
5564
5838
  }
5565
5839
 
5566
5840
  .send:disabled {
5567
5841
  opacity: 0.5;
5568
5842
  cursor: default;
5843
+ transform: none;
5569
5844
  }
5570
5845
 
5571
- /* The composer button doubles as the Stop control while a run is in flight. */
5846
+ /* The composer button doubles as the Stop control while a run is in flight \u2014
5847
+ same circle, different glyph, so nothing moves when a run starts. */
5572
5848
  .send[data-state="running"] {
5573
5849
  background: var(--_muted);
5574
5850
  }
5575
5851
 
5576
- /* \u2500\u2500 File attachments \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
5577
- /* The \u{1F4CE} picker button sits left of the input; hidden until data-attachments-url. */
5578
- .attach-btn {
5579
- border: 1px solid var(--_border);
5580
- border-radius: 8px;
5581
- padding: 0 10px;
5582
- background: var(--_input-bg);
5583
- color: inherit;
5584
- font: inherit;
5585
- cursor: pointer;
5586
- }
5587
-
5588
- .attach-btn:hover {
5589
- border-color: var(--_accent);
5852
+ .send[data-state="idle"] .send-stop,
5853
+ .send[data-state="running"] .send-send {
5854
+ display: none;
5590
5855
  }
5591
5856
 
5592
- .attach-input {
5593
- display: none;
5857
+ /* Glyphs \u2014 one class, painted from the button's own colour so every state
5858
+ (hover, recording, running) carries the icon with it. */
5859
+ .glyph {
5860
+ width: var(--_glyph-size);
5861
+ height: var(--_glyph-size);
5862
+ fill: none;
5863
+ stroke: currentColor;
5864
+ stroke-width: var(--_glyph-stroke);
5865
+ stroke-linecap: round;
5866
+ stroke-linejoin: round;
5594
5867
  }
5595
5868
 
5596
- /* The \u{1F3A4} mic button; shown only once #wireVoice mounts it. */
5597
- .voice-slot {
5598
- display: contents;
5869
+ .glyph--solid {
5870
+ fill: currentColor;
5871
+ stroke: none;
5599
5872
  }
5600
5873
 
5601
- .voice-btn {
5602
- border: 1px solid var(--_border);
5603
- border-radius: 8px;
5604
- padding: 0 10px;
5605
- background: var(--_input-bg);
5606
- color: inherit;
5607
- font: inherit;
5608
- cursor: pointer;
5874
+ /* In an icon holder (header brand, launcher) the glyph takes the holder's size
5875
+ rather than the composer's. */
5876
+ .icon-holder .glyph {
5877
+ width: 100%;
5878
+ height: 100%;
5609
5879
  }
5610
5880
 
5611
- .voice-btn:hover {
5612
- border-color: var(--_accent);
5881
+ /* \u2500\u2500 File attachments \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
5882
+ /* The picker button sits in the composer's tool row; hidden until
5883
+ data-attachments-url. */
5884
+ .attach-input {
5885
+ display: none;
5613
5886
  }
5614
5887
 
5615
- .voice-btn:disabled {
5616
- cursor: default;
5617
- opacity: 0.6;
5888
+ /* The mic button's mount point; filled only once #wireVoice mounts the
5889
+ control. */
5890
+ .voice-slot {
5891
+ display: contents;
5618
5892
  }
5619
5893
 
5620
5894
  /* Recording: a red tint + a gentle pulse so it's clearly "live". */
5621
5895
  .voice-btn[data-state="recording"] {
5622
- border-color: var(--_danger);
5623
5896
  background: var(--_danger);
5624
5897
  color: #ffffff;
5625
5898
  animation: ag-ui-voice-pulse 1.2s ease-in-out infinite;
5626
5899
  }
5627
5900
 
5901
+ .voice-btn[data-state="recording"]:hover {
5902
+ background: var(--_danger);
5903
+ color: #ffffff;
5904
+ }
5905
+
5628
5906
  @keyframes ag-ui-voice-pulse {
5629
5907
  0%, 100% { opacity: 1; }
5630
5908
  50% { opacity: 0.6; }
@@ -6015,16 +6293,35 @@ var STYLES = `
6015
6293
  color: var(--_danger);
6016
6294
  }
6017
6295
 
6018
- /* Chat-history drawer \u2014 a slide-over within the chat panel. */
6296
+ /* Chat-history drawer \u2014 a slide-over within the chat panel.
6297
+ The hidden attribute stays the single source of truth for open/closed (no
6298
+ JS-driven animation state): display is transitioned discretely, so on close
6299
+ the drawer stays displayed for the whole exit and is removed only at the end,
6300
+ which is what lets the backdrop fade and the panel slide out. */
6019
6301
  .drawer {
6020
6302
  position: absolute;
6021
6303
  inset: 0;
6022
6304
  z-index: 5;
6023
6305
  display: flex;
6306
+ transition: visibility var(--_motion) var(--_ease);
6024
6307
  }
6025
6308
 
6309
+ /* Closed. The overlay keeps its box (display, not none) so the backdrop and
6310
+ panel inside it stay rendered and can transition both ways; visibility is
6311
+ what takes the whole subtree out of the tab order, the a11y tree and hit
6312
+ testing at rest, and it holds off until the slide has finished. */
6026
6313
  .drawer[hidden] {
6027
- display: none;
6314
+ display: flex;
6315
+ visibility: hidden;
6316
+ pointer-events: none;
6317
+ }
6318
+
6319
+ .drawer[hidden] .drawer-backdrop {
6320
+ opacity: 0;
6321
+ }
6322
+
6323
+ .drawer[hidden] .drawer-panel {
6324
+ transform: translateX(-100%);
6028
6325
  }
6029
6326
 
6030
6327
  .checkpoints {
@@ -6042,10 +6339,21 @@ var STYLES = `
6042
6339
  box-shadow: 0 6px 24px rgb(0 0 0 / 12%);
6043
6340
  max-height: 60%;
6044
6341
  overflow-y: auto;
6342
+ transform-origin: top center;
6343
+ transition:
6344
+ opacity var(--_motion) var(--_ease),
6345
+ transform var(--_motion) var(--_ease),
6346
+ visibility var(--_motion) var(--_ease);
6045
6347
  }
6046
6348
 
6349
+ /* Same idiom as the drawer: laid out at rest, hidden by visibility, so the
6350
+ popover can animate open and closed. */
6047
6351
  .checkpoints[hidden] {
6048
- display: none;
6352
+ display: flex;
6353
+ visibility: hidden;
6354
+ pointer-events: none;
6355
+ opacity: 0;
6356
+ transform: scale(0.96) translateY(-6px);
6049
6357
  }
6050
6358
 
6051
6359
  .checkpoints-title {
@@ -6107,6 +6415,8 @@ var STYLES = `
6107
6415
  position: absolute;
6108
6416
  inset: 0;
6109
6417
  background: rgba(20, 20, 50, 0.32);
6418
+ opacity: 1;
6419
+ transition: opacity var(--_motion) var(--_ease);
6110
6420
  }
6111
6421
 
6112
6422
  .drawer-panel {
@@ -6119,6 +6429,8 @@ var STYLES = `
6119
6429
  border-right: 1px solid var(--_border);
6120
6430
  box-shadow: var(--_shadow);
6121
6431
  overflow: hidden;
6432
+ transform: none;
6433
+ transition: transform var(--_motion) var(--_ease);
6122
6434
  }
6123
6435
 
6124
6436
  .drawer-header {
@@ -6748,7 +7060,10 @@ var VoiceInput = class {
6748
7060
  this.element.type = "button";
6749
7061
  this.element.className = "voice-btn";
6750
7062
  this.element.setAttribute("part", "voice-button");
6751
- this.element.textContent = "\u{1F3A4}";
7063
+ const glyph = document.createElement("slot");
7064
+ glyph.name = "icon-voice";
7065
+ glyph.innerHTML = ICON_VOICE;
7066
+ this.element.append(glyph);
6752
7067
  this.#setState("idle");
6753
7068
  this.element.addEventListener("click", () => {
6754
7069
  void this.toggle();
@@ -7865,8 +8180,12 @@ var AgUiChat = class extends HTMLElement {
7865
8180
  #attachSlot;
7866
8181
  /** Optional built-in header theme toggle; shown only with `data-theme-toggle`. */
7867
8182
  #themeToggle;
7868
- /** The collapsed-sidebar rail (an expand affordance; shown only for `placement="sidebar"`). */
7869
- #rail;
8183
+ /** What the collapsed widget shrinks to: the floating launcher, or the sidebar rail. */
8184
+ #launcher;
8185
+ /** The launcher's unread badge; hidden at zero, and when the host opts out. */
8186
+ #badge;
8187
+ // Answers that finished while the widget was collapsed. Expanding clears it.
8188
+ #unread = 0;
7870
8189
  /** Empty-state region at the top of the message list; hidden once anything renders. */
7871
8190
  #emptyWrap;
7872
8191
  /** Upload tray; created on connect only when `data-attachments-url` is set. */
@@ -7939,7 +8258,8 @@ var AgUiChat = class extends HTMLElement {
7939
8258
  this.#attachSlot = document.createElement("div");
7940
8259
  this.#voiceSlot = document.createElement("span");
7941
8260
  this.#themeToggle = document.createElement("button");
7942
- this.#rail = document.createElement("button");
8261
+ this.#launcher = document.createElement("button");
8262
+ this.#badge = document.createElement("span");
7943
8263
  this.#emptyWrap = document.createElement("div");
7944
8264
  this.#skillsMenu = new SkillsMenu((skill) => this.#applySkill(skill));
7945
8265
  this.#drawer = new ThreadDrawer({
@@ -8002,6 +8322,7 @@ var AgUiChat = class extends HTMLElement {
8002
8322
  return;
8003
8323
  }
8004
8324
  this.#input.value = "";
8325
+ this.#autoGrow();
8005
8326
  const endpoint = verb === "resume" ? index.resumeUrl(runId) : index.forkUrl(runId);
8006
8327
  const agent = this.agentFactory({
8007
8328
  endpoint,
@@ -8332,7 +8653,7 @@ var AgUiChat = class extends HTMLElement {
8332
8653
  if (this.#readScopedItem(COLLAPSED_KEY) === "1") {
8333
8654
  this.setAttribute("collapsed", "");
8334
8655
  }
8335
- this.#syncRail();
8656
+ this.#syncLauncher();
8336
8657
  this.#initSkills();
8337
8658
  if (this.#storageNs !== "" && this.conversationStore instanceof SessionStorageStore) {
8338
8659
  this.conversationStore = new SessionStorageStore(this.#storageNs);
@@ -8690,12 +9011,14 @@ Use the read_attachment tool with an id to read a file's contents.`
8690
9011
  this.#skillHint.textContent = this.#strings.skillNeeds.replace("{title}", skill.title).replace("{fields}", missing.join(", "));
8691
9012
  this.#skillHint.hidden = false;
8692
9013
  this.#input.value = text2;
9014
+ this.#autoGrow();
8693
9015
  this.#input.focus();
8694
9016
  this.#selectFirstPlaceholder(text2);
8695
9017
  return;
8696
9018
  }
8697
9019
  this.#skillHint.hidden = true;
8698
9020
  this.#input.value = text2;
9021
+ this.#autoGrow();
8699
9022
  if (skill.sendImmediately === false) {
8700
9023
  this.#input.focus();
8701
9024
  return;
@@ -8733,7 +9056,7 @@ Use the read_attachment tool with an id to read a file's contents.`
8733
9056
  this.removeAttribute("collapsed");
8734
9057
  }
8735
9058
  sessionStorage.setItem(this.#storageKey(COLLAPSED_KEY), collapsed ? "1" : "0");
8736
- this.#syncRail();
9059
+ this.#setUnread(0);
8737
9060
  this.dispatchEvent(
8738
9061
  new CustomEvent(TOGGLE_EVENT, {
8739
9062
  detail: { collapsed },
@@ -8742,6 +9065,15 @@ Use the read_attachment tool with an id to read a file's contents.`
8742
9065
  })
8743
9066
  );
8744
9067
  }
9068
+ /**
9069
+ * Answers that finished while the widget was collapsed, and that the user has
9070
+ * therefore not seen. Expanding (or {@link newChat}) clears it. The launcher's
9071
+ * badge renders this; {@link UNREAD_EVENT} announces every change, so a host
9072
+ * chrome can render its own instead.
9073
+ */
9074
+ get unread() {
9075
+ return this.#unread;
9076
+ }
8745
9077
  /** Flip the collapsed state. Bound to the built-in header toggle. */
8746
9078
  toggleCollapsed() {
8747
9079
  this.setCollapsed(!this.collapsed);
@@ -8935,6 +9267,7 @@ Use the read_attachment tool with an id to read a file's contents.`
8935
9267
  this.#resetState();
8936
9268
  this.#threadId = this.conversationStore.threadId();
8937
9269
  this.#setRunning(false);
9270
+ this.#setUnread(0);
8938
9271
  }
8939
9272
  /** Drop the in-memory run + transcript, leaving the thread id untouched. */
8940
9273
  #resetState() {
@@ -9233,17 +9566,27 @@ Use the read_attachment tool with an id to read a file's contents.`
9233
9566
  const inputRow = document.createElement("div");
9234
9567
  inputRow.className = "input-row";
9235
9568
  inputRow.setAttribute("part", "composer");
9569
+ const composer = document.createElement("div");
9570
+ composer.className = "composer";
9571
+ composer.setAttribute("part", "composer-surface");
9572
+ const tools = document.createElement("div");
9573
+ tools.className = "composer-tools";
9574
+ tools.setAttribute("part", "composer-tools");
9236
9575
  this.#input.className = "input";
9237
9576
  this.#input.setAttribute("part", "input");
9238
9577
  this.#input.setAttribute("aria-label", this.#strings.message);
9239
- this.#input.rows = 2;
9578
+ this.#input.rows = 1;
9240
9579
  this.#input.placeholder = this.#strings.inputPlaceholder;
9241
9580
  this.#input.addEventListener("keydown", (event) => this.#onKeydown(event));
9242
9581
  this.#input.addEventListener("input", () => this.#onInput());
9243
9582
  this.#send.className = "send";
9244
9583
  this.#send.type = "button";
9245
9584
  this.#send.setAttribute("part", "send");
9246
- this.#send.textContent = this.#strings.send;
9585
+ this.#send.append(
9586
+ this.#glyphSlot("icon-send", "send-send", ICON_SEND),
9587
+ this.#glyphSlot("icon-stop", "send-stop", ICON_STOP)
9588
+ );
9589
+ this.#send.title = this.#strings.send;
9247
9590
  this.#send.setAttribute("aria-label", this.#strings.send);
9248
9591
  this.#send.dataset["state"] = "idle";
9249
9592
  this.#send.addEventListener("click", () => {
@@ -9259,7 +9602,7 @@ Use the read_attachment tool with an id to read a file's contents.`
9259
9602
  this.#attachButton.className = "attach-btn";
9260
9603
  this.#attachButton.type = "button";
9261
9604
  this.#attachButton.setAttribute("part", "attach-button");
9262
- this.#attachButton.textContent = "\u{1F4CE}";
9605
+ this.#attachButton.append(this.#glyphSlot("icon-attach", "attach-glyph", ICON_ATTACH));
9263
9606
  this.#attachButton.title = this.#strings.attachFiles;
9264
9607
  this.#attachButton.setAttribute("aria-label", this.#strings.attachFiles);
9265
9608
  this.#attachButton.hidden = true;
@@ -9273,7 +9616,9 @@ Use the read_attachment tool with an id to read a file's contents.`
9273
9616
  this.#voiceSlot.className = "voice-slot";
9274
9617
  const footer = document.createElement("slot");
9275
9618
  footer.name = "footer";
9276
- inputRow.append(this.#attachButton, this.#voiceSlot, this.#input, this.#send, this.#fileInput);
9619
+ tools.append(this.#attachButton, this.#voiceSlot, this.#send);
9620
+ composer.append(this.#input, tools);
9621
+ inputRow.append(composer, this.#fileInput);
9277
9622
  this.#chat.append(
9278
9623
  header,
9279
9624
  this.#messages,
@@ -9286,12 +9631,19 @@ Use the read_attachment tool with an id to read a file's contents.`
9286
9631
  this.#drawer.element,
9287
9632
  this.#checkpoints.element
9288
9633
  );
9289
- this.#rail.className = "rail";
9290
- this.#rail.type = "button";
9291
- this.#rail.setAttribute("part", "launcher");
9292
- this.#rail.setAttribute("aria-label", this.#strings.expand);
9293
- this.#rail.append(this.#iconElement("launcher", "launcher-icon", "\u{1F4AC}"));
9294
- this.#rail.addEventListener("click", () => this.setCollapsed(false));
9634
+ this.#launcher.className = "launcher";
9635
+ this.#launcher.type = "button";
9636
+ this.#launcher.setAttribute("part", "launcher");
9637
+ this.#launcher.setAttribute("aria-label", this.#strings.expand);
9638
+ this.#badge.className = "launcher-badge";
9639
+ this.#badge.setAttribute("part", "launcher-badge");
9640
+ this.#badge.setAttribute("aria-hidden", "true");
9641
+ this.#badge.hidden = true;
9642
+ this.#launcher.append(
9643
+ this.#iconElement("launcher", "launcher-icon", ICON_LAUNCHER, this.#launcherIconUrl()),
9644
+ this.#badge
9645
+ );
9646
+ this.#launcher.addEventListener("click", () => this.setCollapsed(false));
9295
9647
  this.#chat.append(
9296
9648
  createResizeHandle({
9297
9649
  axis: () => this.#resizeAxis(),
@@ -9305,7 +9657,7 @@ Use the read_attachment tool with an id to read a file's contents.`
9305
9657
  label: this.#strings.resizePanel
9306
9658
  })
9307
9659
  );
9308
- this.#root.append(style, this.#chat, this.#rail);
9660
+ this.#root.append(style, this.#chat, this.#launcher);
9309
9661
  }
9310
9662
  /**
9311
9663
  * Build a header control button: a named slot a host can project markup into,
@@ -9332,16 +9684,37 @@ Use the read_attachment tool with an id to read a file's contents.`
9332
9684
  return button2;
9333
9685
  }
9334
9686
  /**
9335
- * An icon holder wrapping a `<slot>` so a host can project custom markup; with
9336
- * a `data-icon-url` `<img>` as the slot's fallback, or a glyph when given.
9687
+ * A `<slot>` a host can project its own mark into, falling back to one of the
9688
+ * built-in glyphs. The markup is an author-written constant, never user or
9689
+ * server data, so it is assigned directly rather than sanitised.
9690
+ */
9691
+ #glyphSlot(slotName, className, markup) {
9692
+ const slot = document.createElement("slot");
9693
+ slot.name = slotName;
9694
+ slot.className = className;
9695
+ slot.innerHTML = markup;
9696
+ return slot;
9697
+ }
9698
+ /**
9699
+ * The launcher's own image URL. `data-launcher-icon-url` lets the collapsed
9700
+ * button carry a different mark from the header's — a product logo reads at
9701
+ * 22px in a header bar but rarely at 26px in a circle — and falls back to the
9702
+ * header icon so a single `data-icon-url` still feeds both.
9703
+ */
9704
+ #launcherIconUrl() {
9705
+ return this.getAttribute("data-launcher-icon-url") ?? this.getAttribute("data-icon-url");
9706
+ }
9707
+ /**
9708
+ * An icon holder wrapping a `<slot>` so a host can project custom markup;
9709
+ * with an `<img>` as the slot's fallback when an icon URL is configured, or
9710
+ * the given glyph markup when it is not.
9337
9711
  */
9338
- #iconElement(slotName, part, fallbackGlyph) {
9712
+ #iconElement(slotName, part, fallbackGlyph, iconUrl = this.getAttribute("data-icon-url")) {
9339
9713
  const holder = document.createElement("span");
9340
9714
  holder.className = "icon-holder";
9341
9715
  holder.setAttribute("part", part);
9342
9716
  const slot = document.createElement("slot");
9343
9717
  slot.name = slotName;
9344
- const iconUrl = this.getAttribute("data-icon-url");
9345
9718
  if (iconUrl !== null) {
9346
9719
  const img = document.createElement("img");
9347
9720
  img.className = "icon-img";
@@ -9349,14 +9722,63 @@ Use the read_attachment tool with an id to read a file's contents.`
9349
9722
  img.alt = "";
9350
9723
  slot.append(img);
9351
9724
  } else if (fallbackGlyph !== null) {
9352
- slot.append(document.createTextNode(fallbackGlyph));
9725
+ slot.innerHTML = fallbackGlyph;
9353
9726
  }
9354
9727
  holder.append(slot);
9355
9728
  return holder;
9356
9729
  }
9357
- /** Reflect the collapsed state on the rail's `aria-expanded`. */
9358
- #syncRail() {
9359
- this.#rail.setAttribute("aria-expanded", String(!this.collapsed));
9730
+ /**
9731
+ * Reflect the collapsed state and the unread count on the launcher.
9732
+ *
9733
+ * The count is also the launcher's accessible name: a badge that only exists
9734
+ * as a coloured dot says nothing to a screen reader, and "Expand" alone would
9735
+ * be a lie once answers are waiting behind it.
9736
+ */
9737
+ #syncLauncher() {
9738
+ this.#launcher.setAttribute("aria-expanded", String(!this.collapsed));
9739
+ const unread = this.#unread;
9740
+ this.#badge.textContent = unread > 9 ? "9+" : String(unread);
9741
+ this.#badge.hidden = unread === 0 || !this.#badgeEnabled();
9742
+ const label = this.#badge.hidden ? this.#strings.expand : this.#strings.expandUnread.replace("{count}", String(unread));
9743
+ this.#launcher.setAttribute("aria-label", label);
9744
+ this.#launcher.title = label;
9745
+ }
9746
+ /**
9747
+ * The unread badge, unlike every other affordance here, is on by default:
9748
+ * a collapsed widget is the one state where an answer can arrive with nothing
9749
+ * on screen to say so. `data-unread-badge="false"` turns it off for a host
9750
+ * that drives its own chrome from the `ag-ui-unread` event.
9751
+ */
9752
+ #badgeEnabled() {
9753
+ return this.getAttribute("data-unread-badge") !== "false";
9754
+ }
9755
+ /**
9756
+ * Set the unread count, repaint the badge, and tell the host.
9757
+ *
9758
+ * The count is kept whether or not the badge renders it, so `unread` stays
9759
+ * truthful for a host chrome and switching the badge on mid-session doesn't
9760
+ * start from a number that was never counted.
9761
+ */
9762
+ #setUnread(count) {
9763
+ this.#unread = count;
9764
+ this.#syncLauncher();
9765
+ this.dispatchEvent(
9766
+ new CustomEvent(UNREAD_EVENT, {
9767
+ detail: { unread: count },
9768
+ bubbles: true,
9769
+ composed: true
9770
+ })
9771
+ );
9772
+ }
9773
+ /**
9774
+ * Count an answer the user cannot have seen: one that finished while the
9775
+ * widget was collapsed. Expanding is what marks them read.
9776
+ */
9777
+ #noteUnread() {
9778
+ if (!this.collapsed) {
9779
+ return;
9780
+ }
9781
+ this.#setUnread(this.#unread + 1);
9360
9782
  }
9361
9783
  /** Hide the empty-state region once the message list holds anything else. */
9362
9784
  #updateEmptyState() {
@@ -9366,6 +9788,7 @@ Use the read_attachment tool with an id to read a file's contents.`
9366
9788
  #onInput() {
9367
9789
  this.#skillsMenu.onInput(this.#input.value);
9368
9790
  this.#skillHint.hidden = true;
9791
+ this.#autoGrow();
9369
9792
  }
9370
9793
  #onKeydown(event) {
9371
9794
  if (this.#skillsMenu.onKeydown(event)) {
@@ -9392,14 +9815,32 @@ Use the read_attachment tool with an id to read a file's contents.`
9392
9815
  this.#confirmAbort?.abort();
9393
9816
  this.#client?.cancel();
9394
9817
  }
9395
- /** Swap the composer button between Send (idle) and Stop (running). */
9818
+ /**
9819
+ * Swap the composer button between Send (idle) and Stop (running).
9820
+ *
9821
+ * The glyph is swapped by CSS from `data-state` — both are mounted — so this
9822
+ * only has to move the accessible name, which is the button's whole label now
9823
+ * that it carries no text.
9824
+ */
9396
9825
  #setRunning(running) {
9397
9826
  this.#running = running;
9398
9827
  const label = running ? this.#strings.stop : this.#strings.send;
9399
- this.#send.textContent = label;
9828
+ this.#send.title = label;
9400
9829
  this.#send.setAttribute("aria-label", label);
9401
9830
  this.#send.dataset["state"] = running ? "running" : "idle";
9402
9831
  }
9832
+ /**
9833
+ * Size the field to its content: one row when empty, growing with what is
9834
+ * typed until the CSS ceiling takes over and it scrolls.
9835
+ *
9836
+ * Resetting to `auto` first is what makes it shrink again — `scrollHeight`
9837
+ * never reports less than the current height, so measuring without the reset
9838
+ * would ratchet the composer taller and never back down.
9839
+ */
9840
+ #autoGrow() {
9841
+ this.#input.style.height = "auto";
9842
+ this.#input.style.height = `${this.#input.scrollHeight}px`;
9843
+ }
9403
9844
  async #submit() {
9404
9845
  if (this.#running) {
9405
9846
  return;
@@ -9410,6 +9851,7 @@ Use the read_attachment tool with an id to read a file's contents.`
9410
9851
  return;
9411
9852
  }
9412
9853
  this.#input.value = "";
9854
+ this.#autoGrow();
9413
9855
  if (this.#attachTray?.hasPending() === true) {
9414
9856
  this.#appendNotice(
9415
9857
  "\u{1F4CE}",
@@ -9678,6 +10120,7 @@ Use the read_attachment tool with an id to read a file's contents.`
9678
10120
  }
9679
10121
  attachCopyButtons(bubble, this.#strings);
9680
10122
  this.#streamingBubble = null;
10123
+ this.#noteUnread();
9681
10124
  },
9682
10125
  onToolCall: (call) => {
9683
10126
  this.#hidePending();
@@ -9904,7 +10347,7 @@ function setControlValue(el, value) {
9904
10347
  }
9905
10348
 
9906
10349
  // src/version.ts
9907
- var VERSION = "0.21.0";
10350
+ var VERSION = "0.22.0";
9908
10351
  export {
9909
10352
  ATTACHMENT_EVENT,
9910
10353
  AgUiChat,
@@ -9928,6 +10371,7 @@ export {
9928
10371
  TOOL_CALL_STATUS,
9929
10372
  TOOL_DISPLAY,
9930
10373
  ToolCallCard,
10374
+ UNREAD_EVENT,
9931
10375
  VERSION,
9932
10376
  X_CONFIRM_KEY,
9933
10377
  X_DESTRUCTIVE_KEY,