@artooi/ag-ui-web-component 0.44.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/CHANGELOG.md +182 -1
  2. package/README.md +245 -32
  3. package/dist/ag-ui-web-component.bundle.js +3696 -2798
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +4 -1
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +28 -8
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/core/read_flag.d.ts +11 -0
  10. package/dist/core/read_flag.d.ts.map +1 -0
  11. package/dist/core/read_json_attribute.d.ts +15 -0
  12. package/dist/core/read_json_attribute.d.ts.map +1 -0
  13. package/dist/index.js +5084 -3481
  14. package/dist/index.js.map +4 -4
  15. package/dist/tools/tool_catalog.d.ts +5 -0
  16. package/dist/tools/tool_catalog.d.ts.map +1 -1
  17. package/dist/tools/tool_dispatch.d.ts.map +1 -1
  18. package/dist/ui/charts/chart_styles.d.ts +2 -0
  19. package/dist/ui/charts/chart_styles.d.ts.map +1 -0
  20. package/dist/ui/composer/attachment_styles.d.ts +2 -0
  21. package/dist/ui/composer/attachment_styles.d.ts.map +1 -0
  22. package/dist/ui/composer/build_composer.d.ts +30 -0
  23. package/dist/ui/composer/build_composer.d.ts.map +1 -0
  24. package/dist/ui/composer/composer_styles.d.ts +2 -0
  25. package/dist/ui/composer/composer_styles.d.ts.map +1 -0
  26. package/dist/ui/composer/skill_styles.d.ts +2 -0
  27. package/dist/ui/composer/skill_styles.d.ts.map +1 -0
  28. package/dist/ui/history/history_styles.d.ts +2 -0
  29. package/dist/ui/history/history_styles.d.ts.map +1 -0
  30. package/dist/ui/interrupts/approval_styles.d.ts +2 -0
  31. package/dist/ui/interrupts/approval_styles.d.ts.map +1 -0
  32. package/dist/ui/interrupts/confirmation_styles.d.ts +2 -0
  33. package/dist/ui/interrupts/confirmation_styles.d.ts.map +1 -0
  34. package/dist/ui/interrupts/question_styles.d.ts +2 -0
  35. package/dist/ui/interrupts/question_styles.d.ts.map +1 -0
  36. package/dist/ui/placement/collapse_styles.d.ts +2 -0
  37. package/dist/ui/placement/collapse_styles.d.ts.map +1 -0
  38. package/dist/ui/placement/embedded_styles.d.ts +2 -0
  39. package/dist/ui/placement/embedded_styles.d.ts.map +1 -0
  40. package/dist/ui/placement/launcher_styles.d.ts +2 -0
  41. package/dist/ui/placement/launcher_styles.d.ts.map +1 -0
  42. package/dist/ui/placement/page_styles.d.ts +2 -0
  43. package/dist/ui/placement/page_styles.d.ts.map +1 -0
  44. package/dist/ui/placement/panel_placement.d.ts.map +1 -1
  45. package/dist/ui/placement/preset_styles.d.ts +2 -0
  46. package/dist/ui/placement/preset_styles.d.ts.map +1 -0
  47. package/dist/ui/placement/resize_styles.d.ts +2 -0
  48. package/dist/ui/placement/resize_styles.d.ts.map +1 -0
  49. package/dist/ui/placement/sidebar_styles.d.ts +2 -0
  50. package/dist/ui/placement/sidebar_styles.d.ts.map +1 -0
  51. package/dist/ui/placement/small_viewport_styles.d.ts +2 -0
  52. package/dist/ui/placement/small_viewport_styles.d.ts.map +1 -0
  53. package/dist/ui/placement/surface_for.d.ts +74 -0
  54. package/dist/ui/placement/surface_for.d.ts.map +1 -0
  55. package/dist/ui/progress/run_notice_styles.d.ts +2 -0
  56. package/dist/ui/progress/run_notice_styles.d.ts.map +1 -0
  57. package/dist/ui/progress/thoughts_styles.d.ts +2 -0
  58. package/dist/ui/progress/thoughts_styles.d.ts.map +1 -0
  59. package/dist/ui/progress/tool_call_card.d.ts +8 -0
  60. package/dist/ui/progress/tool_call_card.d.ts.map +1 -1
  61. package/dist/ui/progress/tool_group_styles.d.ts +2 -0
  62. package/dist/ui/progress/tool_group_styles.d.ts.map +1 -0
  63. package/dist/ui/progress/tool_step_group.d.ts +47 -0
  64. package/dist/ui/progress/tool_step_group.d.ts.map +1 -0
  65. package/dist/ui/progress/tool_step_styles.d.ts +2 -0
  66. package/dist/ui/progress/tool_step_styles.d.ts.map +1 -0
  67. package/dist/ui/shell/adopt_styles.d.ts +9 -4
  68. package/dist/ui/shell/adopt_styles.d.ts.map +1 -1
  69. package/dist/ui/shell/build_header.d.ts +40 -0
  70. package/dist/ui/shell/build_header.d.ts.map +1 -0
  71. package/dist/ui/shell/build_launcher.d.ts +20 -0
  72. package/dist/ui/shell/build_launcher.d.ts.map +1 -0
  73. package/dist/ui/shell/frame_styles.d.ts +2 -0
  74. package/dist/ui/shell/frame_styles.d.ts.map +1 -0
  75. package/dist/ui/shell/theme_styles.d.ts +2 -0
  76. package/dist/ui/shell/theme_styles.d.ts.map +1 -0
  77. package/dist/ui/shell/token_styles.d.ts +2 -0
  78. package/dist/ui/shell/token_styles.d.ts.map +1 -0
  79. package/dist/ui/styles.d.ts +1 -1
  80. package/dist/ui/styles.d.ts.map +1 -1
  81. package/dist/ui/themes/classic_styles.d.ts +2 -0
  82. package/dist/ui/themes/classic_styles.d.ts.map +1 -0
  83. package/dist/ui/themes/classic_tokens.d.ts +2 -0
  84. package/dist/ui/themes/classic_tokens.d.ts.map +1 -0
  85. package/dist/ui/themes/code_tokens.d.ts +2 -0
  86. package/dist/ui/themes/code_tokens.d.ts.map +1 -0
  87. package/dist/ui/themes/ledger_styles.d.ts +2 -0
  88. package/dist/ui/themes/ledger_styles.d.ts.map +1 -0
  89. package/dist/ui/themes/ledger_tokens.d.ts +2 -0
  90. package/dist/ui/themes/ledger_tokens.d.ts.map +1 -0
  91. package/dist/ui/themes/modern_styles.d.ts +2 -0
  92. package/dist/ui/themes/modern_styles.d.ts.map +1 -0
  93. package/dist/ui/themes/quiet_styles.d.ts +2 -0
  94. package/dist/ui/themes/quiet_styles.d.ts.map +1 -0
  95. package/dist/ui/themes/scheme_styles.d.ts +2 -0
  96. package/dist/ui/themes/scheme_styles.d.ts.map +1 -0
  97. package/dist/ui/themes/soft_styles.d.ts +2 -0
  98. package/dist/ui/themes/soft_styles.d.ts.map +1 -0
  99. package/dist/ui/themes/soft_tokens.d.ts +2 -0
  100. package/dist/ui/themes/soft_tokens.d.ts.map +1 -0
  101. package/dist/ui/themes/theme_hosts.d.ts +15 -0
  102. package/dist/ui/themes/theme_hosts.d.ts.map +1 -0
  103. package/dist/ui/transcript/attachment_chip_styles.d.ts +2 -0
  104. package/dist/ui/transcript/attachment_chip_styles.d.ts.map +1 -0
  105. package/dist/ui/transcript/build_transcript.d.ts +21 -0
  106. package/dist/ui/transcript/build_transcript.d.ts.map +1 -0
  107. package/dist/ui/transcript/greeting_styles.d.ts +2 -0
  108. package/dist/ui/transcript/greeting_styles.d.ts.map +1 -0
  109. package/dist/ui/transcript/groups_tool_steps.d.ts +13 -0
  110. package/dist/ui/transcript/groups_tool_steps.d.ts.map +1 -0
  111. package/dist/ui/transcript/message_action_styles.d.ts +2 -0
  112. package/dist/ui/transcript/message_action_styles.d.ts.map +1 -0
  113. package/dist/ui/transcript/message_styles.d.ts +2 -0
  114. package/dist/ui/transcript/message_styles.d.ts.map +1 -0
  115. package/dist/ui/transcript/transcript.d.ts +16 -1
  116. package/dist/ui/transcript/transcript.d.ts.map +1 -1
  117. package/dist/ui/transcript/transcript_styles.d.ts +2 -0
  118. package/dist/ui/transcript/transcript_styles.d.ts.map +1 -0
  119. package/dist/ui/ui_strings.d.ts +18 -0
  120. package/dist/ui/ui_strings.d.ts.map +1 -1
  121. package/package.json +2 -2
  122. package/src/constants.ts +4 -1
  123. package/src/core/ag_ui_chat.ts +164 -299
  124. package/src/core/read_flag.ts +13 -0
  125. package/src/core/read_json_attribute.ts +30 -0
  126. package/src/tools/tool_catalog.ts +23 -10
  127. package/src/tools/tool_dispatch.ts +26 -11
  128. package/src/ui/charts/chart_block.ts +9 -9
  129. package/src/ui/charts/chart_styles.ts +63 -0
  130. package/src/ui/composer/attachment_styles.ts +71 -0
  131. package/src/ui/composer/build_composer.ts +112 -0
  132. package/src/ui/composer/composer_styles.ts +174 -0
  133. package/src/ui/composer/skill_styles.ts +120 -0
  134. package/src/ui/history/history_styles.ts +484 -0
  135. package/src/ui/interrupts/approval_styles.ts +52 -0
  136. package/src/ui/interrupts/confirmation_styles.ts +114 -0
  137. package/src/ui/interrupts/question_styles.ts +73 -0
  138. package/src/ui/placement/collapse_styles.ts +137 -0
  139. package/src/ui/placement/embedded_styles.ts +14 -0
  140. package/src/ui/placement/launcher_styles.ts +189 -0
  141. package/src/ui/placement/page_styles.ts +47 -0
  142. package/src/ui/placement/panel_placement.ts +3 -12
  143. package/src/ui/placement/preset_styles.ts +33 -0
  144. package/src/ui/placement/resize_styles.ts +186 -0
  145. package/src/ui/placement/sidebar_styles.ts +88 -0
  146. package/src/ui/placement/small_viewport_styles.ts +56 -0
  147. package/src/ui/placement/surface_for.ts +135 -0
  148. package/src/ui/progress/run_notice_styles.ts +52 -0
  149. package/src/ui/progress/thoughts_styles.ts +65 -0
  150. package/src/ui/progress/tool_call_card.ts +11 -0
  151. package/src/ui/progress/tool_group_styles.ts +232 -0
  152. package/src/ui/progress/tool_step_group.ts +269 -0
  153. package/src/ui/progress/tool_step_styles.ts +472 -0
  154. package/src/ui/shell/adopt_styles.ts +11 -5
  155. package/src/ui/shell/build_header.ts +108 -0
  156. package/src/ui/shell/build_launcher.ts +50 -0
  157. package/src/ui/shell/frame_styles.ts +119 -0
  158. package/src/ui/shell/theme_styles.ts +11 -0
  159. package/src/ui/shell/token_styles.ts +406 -0
  160. package/src/ui/styles.ts +104 -3774
  161. package/src/ui/themes/classic_styles.ts +32 -0
  162. package/src/ui/themes/classic_tokens.ts +50 -0
  163. package/src/ui/themes/code_tokens.ts +21 -0
  164. package/src/ui/themes/ledger_styles.ts +168 -0
  165. package/src/ui/themes/ledger_tokens.ts +32 -0
  166. package/src/ui/themes/modern_styles.ts +80 -0
  167. package/src/ui/themes/quiet_styles.ts +115 -0
  168. package/src/ui/themes/scheme_styles.ts +17 -0
  169. package/src/ui/themes/soft_styles.ts +148 -0
  170. package/src/ui/themes/soft_tokens.ts +28 -0
  171. package/src/ui/themes/theme_hosts.ts +14 -0
  172. package/src/ui/transcript/attachment_chip_styles.ts +116 -0
  173. package/src/ui/transcript/build_transcript.ts +67 -0
  174. package/src/ui/transcript/greeting_styles.ts +171 -0
  175. package/src/ui/transcript/groups_tool_steps.ts +16 -0
  176. package/src/ui/transcript/message_action_styles.ts +155 -0
  177. package/src/ui/transcript/message_styles.ts +262 -0
  178. package/src/ui/transcript/transcript.ts +109 -3
  179. package/src/ui/transcript/transcript_styles.ts +140 -0
  180. package/src/ui/ui_strings.ts +31 -0
  181. package/src/version.ts +1 -1
  182. package/dist/ui/placement/is_collapsible_placement.d.ts +0 -18
  183. package/dist/ui/placement/is_collapsible_placement.d.ts.map +0 -1
  184. package/dist/ui/placement/is_draggable_placement.d.ts +0 -9
  185. package/dist/ui/placement/is_draggable_placement.d.ts.map +0 -1
  186. package/src/ui/placement/is_collapsible_placement.ts +0 -19
  187. package/src/ui/placement/is_draggable_placement.ts +0 -23
@@ -4,11 +4,7 @@ import {
4
4
  CHART_ACTIVITY_TYPE,
5
5
  COMPACTION_ACTIVITY_TYPE,
6
6
  CUSTOM_AGENT_EVENT,
7
- ICON_ATTACH,
8
- ICON_LAUNCHER,
9
7
  ICON_MOON,
10
- ICON_SEND,
11
- ICON_STOP,
12
8
  ICON_SUN,
13
9
  MESSAGE_ROLE,
14
10
  STATE_EVENT,
@@ -37,6 +33,7 @@ import { renderChart } from "../ui/charts/chart_block.js";
37
33
  import { chartSpecFrom } from "../ui/charts/chart_spec_from.js";
38
34
  import { CHART_TOOL_NAME, createChartTool } from "../ui/charts/chart_tool.js";
39
35
  import { autoGrow } from "../ui/composer/auto_grow.js";
36
+ import { buildComposer } from "../ui/composer/build_composer.js";
40
37
  import { ComposerAttachments } from "../ui/composer/composer_attachments.js";
41
38
  import { ComposerVoice } from "../ui/composer/composer_voice.js";
42
39
  import { SkillsMenu } from "../ui/composer/skills_menu.js";
@@ -51,23 +48,23 @@ import { type ApprovalRenderer } from "../ui/interrupts/approval_card.js";
51
48
  import { PendingDecision } from "../ui/interrupts/pending_decision.js";
52
49
  // biome-ignore lint/style/useImportType: the emitted declaration file copies this form
53
50
  import { type QuestionRenderer } from "../ui/interrupts/question_card.js";
54
- import { isCollapsiblePlacement } from "../ui/placement/is_collapsible_placement.js";
55
- import { isDraggablePlacement } from "../ui/placement/is_draggable_placement.js";
56
51
  import { PanelPlacement } from "../ui/placement/panel_placement.js";
52
+ import { surfaceFor } from "../ui/placement/surface_for.js";
57
53
  import { RunAnnouncer } from "../ui/progress/run_announcer.js";
58
54
  import { renderRunNotice } from "../ui/progress/run_notice.js";
59
55
  import { SubAgentProgress } from "../ui/progress/subagent_progress.js";
60
56
  // biome-ignore lint/style/useImportType: the emitted declaration file copies this form
61
57
  import { type ToolDisplayMode, type ToolPayloadFormatter } from "../ui/progress/tool_call_card.js";
62
58
  import { adoptStyles } from "../ui/shell/adopt_styles.js";
63
- import { glyphSlot } from "../ui/shell/glyph_slot.js";
64
- import { headerButton } from "../ui/shell/header_button.js";
59
+ import { buildHeader } from "../ui/shell/build_header.js";
60
+ import { buildLauncher } from "../ui/shell/build_launcher.js";
65
61
  import { iconElement } from "../ui/shell/icon_element.js";
66
62
  import { isUnreadBadgeEnabled } from "../ui/shell/is_unread_badge_enabled.js";
67
63
  import { readLauncherIconUrl } from "../ui/shell/read_launcher_icon_url.js";
68
64
  import { AnswerActions } from "../ui/transcript/answer_actions.js";
69
65
  import { AnswerStream } from "../ui/transcript/answer_stream.js";
70
66
  import { renderAttachmentChips } from "../ui/transcript/attachment_chips.js";
67
+ import { buildTranscript } from "../ui/transcript/build_transcript.js";
71
68
  import { renderStarterChips } from "../ui/transcript/starter_chips.js";
72
69
  import { renderSuggestionChips } from "../ui/transcript/suggestion_chips.js";
73
70
  import { Transcript } from "../ui/transcript/transcript.js";
@@ -91,6 +88,8 @@ import type { SubmitDetail } from "./events/submit_detail.js";
91
88
  import type { ToggleDetail } from "./events/toggle_detail.js";
92
89
  import type { UnreadDetail } from "./events/unread_detail.js";
93
90
  import type { MessageRole } from "./message_role.js";
91
+ import { readFlag } from "./read_flag.js";
92
+ import { readJsonAttribute } from "./read_json_attribute.js";
94
93
  import { readMaxToolRounds } from "./read_max_tool_rounds.js";
95
94
  import { RemoteConversationStore } from "./remote_conversation_store.js";
96
95
  import { RunHandlers } from "./run_handlers.js";
@@ -145,9 +144,34 @@ function isCredentialsMode(value: string): value is RequestCredentials {
145
144
  /** Per-tab persistence key for the collapsed state (survives MPA reloads). */
146
145
  const COLLAPSED_KEY = "ag-ui-chat:collapsed";
147
146
 
148
- /** Per-tab persistence key for the built-in theme toggle. */
147
+ /**
148
+ * Persistence key for the built-in theme toggle. It holds the user's scheme
149
+ * word only: the theme name belongs to the host's markup, and one saved value
150
+ * is restored on every page that shares the key.
151
+ */
149
152
  const THEME_KEY = "ag-ui-chat:theme";
150
153
 
154
+ /** The scheme words of the `theme` attribute; every other word names a theme. */
155
+ const SCHEME_WORDS: readonly string[] = ["light", "dark", "auto"];
156
+
157
+ /**
158
+ * The last scheme word in a saved value, or `undefined` when it names none. A
159
+ * value saved before only the scheme was kept (`"soft dark"`) still gives up
160
+ * just its scheme.
161
+ */
162
+ function savedScheme(saved: string): string | undefined {
163
+ return saved
164
+ .split(/\s+/)
165
+ .filter((word) => SCHEME_WORDS.includes(word))
166
+ .at(-1);
167
+ }
168
+
169
+ /** `theme`'s words with its scheme word, if any, replaced by `scheme`. */
170
+ function withScheme(theme: string, scheme: string): string {
171
+ const names = theme.split(/\s+/).filter((word) => word !== "" && !SCHEME_WORDS.includes(word));
172
+ return [...names, scheme].join(" ");
173
+ }
174
+
151
175
  /**
152
176
  * `<ag-ui-chat>` — a framework-free chat sidebar Web Component over AG-UI.
153
177
  *
@@ -450,6 +474,10 @@ export class AgUiChat extends HTMLElement {
450
474
  });
451
475
 
452
476
  readonly #root: ShadowRoot;
477
+ /** The built-in stylesheet once rendered, kept so `extraStyles` can re-adopt beside it. */
478
+ #builtInSheet: CSSStyleSheet | null = null;
479
+ /** The host's own sheets, adopted after the built-in one. */
480
+ #extraStyles: readonly CSSStyleSheet[] = [];
453
481
  /** The screen-reader-only status region the run and its tool calls report into. */
454
482
  readonly #announcer = new RunAnnouncer();
455
483
  /** Return-to-foot affordance, shown only once something has been missed. */
@@ -739,6 +767,7 @@ export class AgUiChat extends HTMLElement {
739
767
  askUser: () => this.askUser,
740
768
  askUserRenderer: () => this.askUserRenderer,
741
769
  decision: this.#decision,
770
+ setAsking: (callId, asking) => this.#transcript.setAsking(callId, asking),
742
771
  ensureGroup: () => this.#transcript.ensureGroup(),
743
772
  strings: () => this.#strings,
744
773
  hidePending: () => this.#transcript.hidePending(),
@@ -792,6 +821,7 @@ export class AgUiChat extends HTMLElement {
792
821
  resolveTool: (name) => this.#tools.resolve(name),
793
822
  toolSummaries: () => this.toolSummaries,
794
823
  serverSummary: (name) => this.#tools.summary(name),
824
+ toolDisplay: () => this.toolDisplay,
795
825
  // A thunk over the live property, not the property itself: the card keeps
796
826
  // this for the life of the call, and the result region is filled when the
797
827
  // tool settles -- which can be long after a host set the hook.
@@ -839,7 +869,7 @@ export class AgUiChat extends HTMLElement {
839
869
  root: this.#root,
840
870
  connected: () => this.#connected,
841
871
  collapsed: () => this.collapsed,
842
- collapsible: () => isCollapsiblePlacement(this.getAttribute("placement")),
872
+ collapsible: () => surfaceFor(this.getAttribute("placement")).collapseTo !== null,
843
873
  strings: () => this.#strings,
844
874
  readPreference: (base) => this.#storage.readPreference(base),
845
875
  writePreference: (base, value) => this.#storage.writePreference(base, value),
@@ -912,8 +942,8 @@ export class AgUiChat extends HTMLElement {
912
942
  hint: this.#composerHint,
913
943
  strings: () => this.#strings,
914
944
  context: () => this.skillContext(),
915
- flag: (name) => this.#flag(name),
916
- readJsonAttribute: (name) => this.#readJsonAttribute(name),
945
+ flag: (name) => readFlag(this, name),
946
+ readJsonAttribute: (name) => readJsonAttribute(this, name),
917
947
  fetchInit: (url) => this.#fetchInit(url),
918
948
  send: (content) => {
919
949
  void this.sendMessage(content);
@@ -1055,7 +1085,7 @@ export class AgUiChat extends HTMLElement {
1055
1085
  // not just stop offering it: the control is gone from the header the
1056
1086
  // moment the attribute changes, so a panel collapsed under the previous
1057
1087
  // placement would have no way back.
1058
- if (!isCollapsiblePlacement(this.getAttribute("placement")) && this.collapsed) {
1088
+ if (surfaceFor(this.getAttribute("placement")).collapseTo === null && this.collapsed) {
1059
1089
  this.setCollapsed(false);
1060
1090
  }
1061
1091
  // Placement also moves the panel, so the edges its layout holds still
@@ -1349,28 +1379,57 @@ export class AgUiChat extends HTMLElement {
1349
1379
 
1350
1380
  /**
1351
1381
  * How much detail tool-call cards show, from the `data-tool-display`
1352
- * attribute (`minimal` / `inline` / `compact` / `full`). Defaults to `full`.
1382
+ * attribute (`grouped` / `minimal` / `inline` / `compact` / `full`).
1383
+ * Defaults to `grouped`, for an absent attribute and for a value the
1384
+ * element does not know; `full` is the per-call card default before
1385
+ * `grouped` existed.
1353
1386
  *
1354
- * Applied by the shadow CSS from the attribute itself, so changing it
1355
- * restyles every card already in the transcript rather than only the ones
1356
- * built afterwards.
1387
+ * The four card modes are applied by the shadow CSS from the attribute
1388
+ * itself, so changing between them restyles every card already in the
1389
+ * transcript. Whether calls fold into a step group is decided as each card
1390
+ * is built: switching to `grouped` folds only the calls drawn afterwards,
1391
+ * and switching away flattens the existing groups, again by CSS.
1357
1392
  */
1358
1393
  get toolDisplay(): ToolDisplayMode {
1359
1394
  const attr = this.getAttribute("data-tool-display");
1360
1395
  if (
1361
1396
  attr === TOOL_DISPLAY.INLINE ||
1362
1397
  attr === TOOL_DISPLAY.MINIMAL ||
1363
- attr === TOOL_DISPLAY.COMPACT
1398
+ attr === TOOL_DISPLAY.COMPACT ||
1399
+ attr === TOOL_DISPLAY.FULL
1364
1400
  ) {
1365
1401
  return attr;
1366
1402
  }
1367
- return TOOL_DISPLAY.FULL;
1403
+ return TOOL_DISPLAY.GROUPED;
1368
1404
  }
1369
1405
 
1370
1406
  set toolDisplay(value: ToolDisplayMode) {
1371
1407
  this.setAttribute("data-tool-display", value);
1372
1408
  }
1373
1409
 
1410
+ /**
1411
+ * The host's own constructed stylesheets, adopted after the built-in one.
1412
+ *
1413
+ * The escape hatch for structure that neither the tokens nor the parts
1414
+ * reach. Per element, like the built-in sheet: two elements on one page can
1415
+ * carry different sheets. Constructed sheets are not subject to a
1416
+ * `style-src` policy, so this works under the same strict CSP the
1417
+ * built-in sheet does. Setting it re-adopts at once; the array is copied, so
1418
+ * mutating the one passed in changes nothing until it is set again.
1419
+ *
1420
+ * Unstable: reaches shadow class names, which may change in any minor.
1421
+ */
1422
+ get extraStyles(): readonly CSSStyleSheet[] {
1423
+ return this.#extraStyles;
1424
+ }
1425
+
1426
+ set extraStyles(sheets: readonly CSSStyleSheet[]) {
1427
+ this.#extraStyles = [...sheets];
1428
+ if (this.#builtInSheet !== null) {
1429
+ this.#root.adoptedStyleSheets = [this.#builtInSheet, ...this.#extraStyles];
1430
+ }
1431
+ }
1432
+
1374
1433
  connectedCallback(): void {
1375
1434
  // A fresh signal per connection: disconnecting aborted the last one, and
1376
1435
  // with it every listener the last connection added.
@@ -1405,12 +1464,14 @@ export class AgUiChat extends HTMLElement {
1405
1464
  // Resolve the string table before rendering any chrome (defaults are the
1406
1465
  // floor; `data-strings` then the `strings` property layer over them).
1407
1466
  this.#strings = mergeUiStrings({ ...this.#readStringOverrides(), ...this.strings });
1408
- // Restore a theme the built-in toggle persisted last visit (opt-in only, so
1409
- // it never overrides a host that drives `theme` itself).
1467
+ // Restore the scheme the built-in toggle persisted last visit onto the
1468
+ // markup's theme name (opt-in only, so it never overrides a host that
1469
+ // drives `theme` itself).
1410
1470
  if (this.getAttribute("data-theme-toggle") !== null) {
1411
1471
  const saved = this.#storage.readPreference(THEME_KEY);
1412
- if (saved !== null) {
1413
- this.setAttribute("theme", saved);
1472
+ const scheme = saved === null ? undefined : savedScheme(saved);
1473
+ if (scheme !== undefined) {
1474
+ this.setAttribute("theme", withScheme(this.getAttribute("theme") ?? "", scheme));
1414
1475
  }
1415
1476
  }
1416
1477
  this.#render();
@@ -1420,7 +1481,7 @@ export class AgUiChat extends HTMLElement {
1420
1481
  // namespaced per instance but not per placement, so a tab that collapsed a
1421
1482
  // floating panel and later loaded the same instance as a page would restore
1422
1483
  // a state that placement has no way out of.
1423
- if (isCollapsiblePlacement(this.getAttribute("placement")) && this.#startsCollapsed()) {
1484
+ if (surfaceFor(this.getAttribute("placement")).collapseTo !== null && this.#startsCollapsed()) {
1424
1485
  this.setAttribute("collapsed", "");
1425
1486
  }
1426
1487
  this.#syncLauncher();
@@ -1558,53 +1619,9 @@ export class AgUiChat extends HTMLElement {
1558
1619
  this.#announcer.dispose();
1559
1620
  }
1560
1621
 
1561
- /**
1562
- * Read an opt-in flag attribute the way HTML reads a boolean attribute.
1563
- *
1564
- * Present means on: bare (`data-prompt-chips`), empty (`=""`), or any value
1565
- * except the literal `"false"`. Comparing against `"true"` instead would make
1566
- * the bare spelling every native boolean attribute uses silently disable the
1567
- * feature it names. `="false"` still turns it off.
1568
- */
1569
- #flag(name: string): boolean {
1570
- const value = this.getAttribute(name);
1571
- return value !== null && value !== "false";
1572
- }
1573
-
1574
- /**
1575
- * Parse a JSON-valued attribute, saying so when it will not parse.
1576
- *
1577
- * `null` for an absent attribute, and `null` again for one that is not JSON --
1578
- * but not quietly the second time. Quoting JSON inside an HTML attribute is
1579
- * fiddly, and the result of getting it wrong is indistinguishable from the
1580
- * feature being switched off: no chips appear, or the strings stay English,
1581
- * with nothing anywhere saying why. That is the same failure `data-paste-attach`
1582
- * already reports for a value it cannot read.
1583
- *
1584
- * Console only. A page author's typo is not the reader's business, nothing the
1585
- * reader did was refused, and the degraded widget is still perfectly usable.
1586
- */
1587
- #readJsonAttribute(name: string): unknown {
1588
- const raw = this.getAttribute(name);
1589
- if (raw === null) {
1590
- return null;
1591
- }
1592
- try {
1593
- return JSON.parse(raw);
1594
- } catch {
1595
- console.warn(
1596
- `<ag-ui-chat>: ${name} is not valid JSON, so it was ignored entirely and ` +
1597
- "the built-in default is being used. Check the quoting -- JSON inside " +
1598
- "an HTML attribute needs single quotes around the attribute value, or " +
1599
- "its own double quotes escaped.",
1600
- );
1601
- return null;
1602
- }
1603
- }
1604
-
1605
1622
  /** Parse the inline `data-strings` JSON overrides (empty when absent/malformed). */
1606
1623
  #readStringOverrides(): Partial<UiStrings> {
1607
- const parsed = this.#readJsonAttribute("data-strings");
1624
+ const parsed = readJsonAttribute(this, "data-strings");
1608
1625
  // A JSON number or string parses fine and overrides nothing. Not warned
1609
1626
  // about separately: it is the same "this attribute did not take effect"
1610
1627
  // as a parse failure, and the warning above already covers the spelling
@@ -1723,7 +1740,7 @@ export class AgUiChat extends HTMLElement {
1723
1740
  * its own chrome.
1724
1741
  */
1725
1742
  setCollapsed(collapsed: boolean, options: { readonly announce?: boolean } = {}): void {
1726
- if (collapsed && !isCollapsiblePlacement(this.getAttribute("placement"))) {
1743
+ if (collapsed && surfaceFor(this.getAttribute("placement")).collapseTo === null) {
1727
1744
  return;
1728
1745
  }
1729
1746
  // Announced before the state changes, so the notice is written into a
@@ -1800,7 +1817,8 @@ export class AgUiChat extends HTMLElement {
1800
1817
  return stored === "1";
1801
1818
  }
1802
1819
  return (
1803
- isDraggablePlacement(this.getAttribute("placement")) && !this.hasAttribute("data-start-open")
1820
+ surfaceFor(this.getAttribute("placement")).startsCollapsed &&
1821
+ !this.hasAttribute("data-start-open")
1804
1822
  );
1805
1823
  }
1806
1824
 
@@ -1838,15 +1856,21 @@ export class AgUiChat extends HTMLElement {
1838
1856
  }
1839
1857
 
1840
1858
  /**
1841
- * Flip the `theme` attribute between `light` and `dark` and persist the choice
1842
- * per tab. Bound to the optional built-in header theme toggle
1843
- * (`data-theme-toggle`); any non-dark theme (incl. `auto` / `code`) flips to
1844
- * `dark` first.
1859
+ * Flip the scheme word of the `theme` attribute between `light` and `dark`
1860
+ * and persist the choice per tab. Bound to the optional built-in header theme
1861
+ * toggle (`data-theme-toggle`). The theme name and any host words are kept:
1862
+ * `soft` becomes `soft dark`, and `soft dark` becomes `soft light`. Any
1863
+ * scheme but `dark` (including `auto`) flips to `dark` first.
1845
1864
  */
1846
1865
  toggleTheme(): void {
1847
- const next = this.getAttribute("theme") === "dark" ? "light" : "dark";
1848
- this.setAttribute("theme", next);
1849
- this.#storage.writePreference(THEME_KEY, next);
1866
+ // Only the scheme word flips, so a named theme survives the toggle:
1867
+ // "soft" becomes "soft dark", and "soft dark" becomes "soft light". And
1868
+ // only the scheme word is saved: the name is the host's, and a name saved
1869
+ // on one page would replace another page's theme on restore.
1870
+ const theme = this.getAttribute("theme") ?? "";
1871
+ const scheme = theme.split(/\s+/).includes("dark") ? "light" : "dark";
1872
+ this.setAttribute("theme", withScheme(theme, scheme));
1873
+ this.#storage.writePreference(THEME_KEY, scheme);
1850
1874
  this.#syncThemeGlyph();
1851
1875
  }
1852
1876
 
@@ -1964,7 +1988,7 @@ export class AgUiChat extends HTMLElement {
1964
1988
 
1965
1989
  /** Reflect the current theme on the toggle: show the destination's glyph. */
1966
1990
  #syncThemeGlyph(): void {
1967
- const dark = this.getAttribute("theme") === "dark";
1991
+ const dark = (this.getAttribute("theme") ?? "").split(/\s+/).includes("dark");
1968
1992
  // Replaced wholesale rather than toggling a class: the two marks are
1969
1993
  // different paths, not one path in two states, and the slot has to keep
1970
1994
  // working so a host can still supply its own.
@@ -2238,96 +2262,44 @@ export class AgUiChat extends HTMLElement {
2238
2262
  this.#chat.className = "chat";
2239
2263
  this.#chat.setAttribute("part", "panel");
2240
2264
 
2241
- const header = document.createElement("div");
2242
- header.className = "header";
2243
- header.setAttribute("part", "header");
2244
-
2245
- const title = this.#title;
2246
- title.className = "header-title";
2247
- title.setAttribute("part", "title");
2248
- title.textContent = this.getAttribute("title-text") ?? this.#strings.title;
2249
-
2250
- // Optional header icon: a slot (any markup) with a `data-icon-url` <img>
2251
- // fallback. Rendered only when one of the two is provided, so the header has
2252
- // no phantom gap otherwise.
2253
- if (
2254
- this.querySelector('[slot="icon"]') !== null ||
2255
- this.getAttribute("data-icon-url") !== null
2256
- ) {
2257
- header.append(iconElement("icon", "icon", null, this.getAttribute("data-icon-url")));
2258
- }
2259
-
2260
- // A coarse slot for host-provided header actions, between title and controls.
2261
- const headerActions = document.createElement("slot");
2262
- headerActions.name = "header-actions";
2263
-
2264
- const controls = document.createElement("div");
2265
- controls.className = "header-controls";
2266
- controls.setAttribute("part", "header-controls");
2267
-
2268
- // Both controls delegate to the public methods, so a host chrome driving
2269
- // them imperatively takes exactly the path the built-in button takes.
2270
- const history = headerButton("history", this.#strings.chatHistory, "☰");
2271
- history.addEventListener("click", () => this.openThreads());
2272
-
2273
- // ↺ rather than ⭯: the same idea in a glyph that has a font behind it in
2274
- // every browser. The obscure one rendered as an unreadable mark at 14px, and a
2275
- // header control nobody can name is one nobody presses.
2276
- const checkpoints = headerButton("checkpoints", this.#strings.checkpoints, "↺");
2277
- checkpoints.addEventListener("click", () => this.toggleCheckpoints());
2278
-
2279
- const newChat = headerButton("new", this.#strings.newChat, "✚");
2280
- newChat.addEventListener("click", () => this.newChat());
2281
-
2282
- const collapse = headerButton("collapse", this.#strings.collapse, "—");
2283
- collapse.addEventListener("click", () => this.toggleCollapsed());
2284
-
2285
- // Only offered when the server actually indexes runs — without
2286
- // `data-runs-url` there is nothing to continue and the button would open
2287
- // a permanently empty panel. Asks the history's run index rather than
2288
- // re-testing the attribute, so "configured" means one thing everywhere (an
2289
- // empty value is unset, not a relative URL to the current page).
2290
- if (this.#history.runs() !== null) {
2291
- controls.append(history, checkpoints, newChat);
2292
- } else {
2293
- controls.append(history, newChat);
2294
- }
2295
- // Optional built-in theme toggle: off unless the host opts in, so
2296
- // it never competes with a host-supplied switch in `slot="header-actions"`.
2297
- if (this.getAttribute("data-theme-toggle") !== null) {
2298
- this.#themeToggle.type = "button";
2299
- this.#themeToggle.className = "header-btn header-btn--theme";
2300
- this.#themeToggle.setAttribute("part", "header-button theme-toggle");
2301
- this.#themeToggle.title = this.#strings.toggleTheme;
2302
- this.#themeToggle.setAttribute("aria-label", this.#strings.toggleTheme);
2303
- this.#themeToggle.addEventListener("click", () => this.toggleTheme(), { signal });
2265
+ const titleText = this.getAttribute("title-text") ?? this.#strings.title;
2266
+ const themed = this.getAttribute("data-theme-toggle") !== null;
2267
+ const { header, checkpoints } = buildHeader({
2268
+ host: this,
2269
+ title: this.#title,
2270
+ titleText,
2271
+ iconUrl: this.getAttribute("data-icon-url"),
2272
+ strings: this.#strings,
2273
+ // Asks the history's run index rather than re-testing the attribute, so
2274
+ // "configured" means one thing everywhere (an empty value is unset, not a
2275
+ // relative URL to the current page).
2276
+ withCheckpoints: this.#history.runs() !== null,
2277
+ themeToggle: themed ? this.#themeToggle : null,
2278
+ signal,
2279
+ onHistory: () => this.openThreads(),
2280
+ onCheckpoints: () => this.toggleCheckpoints(),
2281
+ onNewChat: () => this.newChat(),
2282
+ onCollapse: () => this.toggleCollapsed(),
2283
+ onToggleTheme: () => this.toggleTheme(),
2284
+ });
2285
+ if (themed) {
2304
2286
  this.#syncThemeGlyph();
2305
- controls.append(this.#themeToggle);
2306
2287
  }
2307
- controls.append(collapse);
2308
- header.append(title, headerActions, controls);
2309
2288
 
2310
2289
  // A panel is a window and a header is its title bar.
2311
2290
  this.#placement.enablePanelDrag(header);
2312
2291
 
2313
- this.#messages.className = "messages";
2314
- this.#messages.setAttribute("part", "messages");
2315
- this.#messages.setAttribute("role", "log");
2316
- // NOT a live region. The streaming bubble's innerHTML is replaced inside
2317
- // this element on every animation frame, and `role="log"` already implies
2318
- // polite announcement whose default `aria-relevant` includes text
2319
- // additions -- so a screen reader was asked to re-announce the whole answer
2320
- // tens of times as it streamed. `aria-live="off"` is an explicit override
2321
- // of the role's implicit value, which is why the role can stay: the log
2322
- // semantics are what let the transcript be navigated as one, and only the
2323
- // announcing is the defect. Status goes to #announcer instead.
2324
- this.#messages.setAttribute("aria-live", "off");
2325
- this.#messages.setAttribute("aria-label", this.#strings.conversation);
2326
-
2327
- this.#jumpButton.className = "jump-latest";
2328
- this.#jumpButton.type = "button";
2329
- this.#jumpButton.setAttribute("part", "jump-latest");
2330
- this.#jumpButton.textContent = this.#strings.jumpToLatest;
2292
+ buildTranscript({
2293
+ messages: this.#messages,
2294
+ jumpButton: this.#jumpButton,
2295
+ emptyWrap: this.#emptyWrap,
2296
+ greetingText: this.#greetingText,
2297
+ starters: renderStarterChips(this, this.#strings, (prompt) => {
2298
+ void this.sendMessage(prompt);
2299
+ }),
2300
+ strings: this.#strings,
2301
+ });
2302
+ this.#syncGreeting();
2331
2303
 
2332
2304
  // The quote offer, and the transcript's settled selections it listens for.
2333
2305
  this.#excerpts.mount(signal);
@@ -2338,82 +2310,25 @@ export class AgUiChat extends HTMLElement {
2338
2310
 
2339
2311
  this.#announcer.mount();
2340
2312
 
2341
- // Empty-state region: a host slot at the top of the list, hidden as soon as
2342
- // anything renders.
2343
- this.#emptyWrap.className = "empty";
2344
- this.#emptyWrap.setAttribute("part", "empty");
2345
- // The greeting heads the empty state, in a slot of its own: a host that
2346
- // replaces the starters keeps the greeting, and a host that replaces the
2347
- // greeting keeps the starters. A div rather than a heading, because this
2348
- // sits inside somebody else's page, which owns the document outline.
2349
- const greeting = document.createElement("div");
2350
- greeting.className = "greeting";
2351
- greeting.setAttribute("part", "greeting");
2352
- const greetingSlot = document.createElement("slot");
2353
- greetingSlot.name = "greeting";
2354
- greetingSlot.append(this.#greetingText);
2355
- greeting.append(greetingSlot);
2356
- this.#syncGreeting();
2357
- const emptySlot = document.createElement("slot");
2358
- emptySlot.name = "empty";
2359
- // Fallback content, so a host that slots its own gets exactly that and
2360
- // nothing of ours: the starters live *inside* the slot rather than beside
2361
- // it, which is the difference between an offer and an imposition.
2362
- const starters = renderStarterChips(this, this.#strings, (prompt) => {
2363
- void this.sendMessage(prompt);
2364
- });
2365
- if (starters !== null) {
2366
- emptySlot.append(starters);
2367
- }
2368
- this.#emptyWrap.replaceChildren(greeting, emptySlot);
2369
- this.#queuedRow.className = "queued";
2370
- this.#queuedRow.setAttribute("part", "queued");
2371
- this.#queuedRow.setAttribute("role", "group");
2372
- this.#queuedRow.setAttribute("aria-label", this.#strings.queued);
2373
- this.#queuedRow.hidden = true;
2374
2313
  this.#messages.append(this.#emptyWrap);
2375
2314
  // Stamped from the first frame, so a page that mounts empty is laid out as
2376
2315
  // empty rather than switching to it on the first change.
2377
2316
  this.#transcript.updateEmptyState();
2378
2317
 
2379
- const inputRow = document.createElement("div");
2380
- inputRow.className = "input-row";
2381
- inputRow.setAttribute("part", "composer");
2382
-
2383
- // One bordered surface holds the field and the tool row under it, so the
2384
- // icon buttons stop competing with the field for weight.
2385
- const composer = document.createElement("div");
2386
- composer.className = "composer";
2387
- composer.setAttribute("part", "composer-surface");
2388
-
2389
- const tools = document.createElement("div");
2390
- tools.className = "composer-tools";
2391
- tools.setAttribute("part", "composer-tools");
2392
-
2393
- this.#input.className = "input";
2394
- this.#input.setAttribute("part", "input");
2395
- this.#input.setAttribute("aria-label", this.#strings.message);
2396
- this.#input.rows = 1;
2397
- this.#input.placeholder = this.#strings.inputPlaceholder;
2398
- this.#input.addEventListener("keydown", (event) => this.#onKeydown(event), { signal });
2399
- this.#input.addEventListener("input", () => this.#onInput(), { signal });
2400
-
2401
- // Icon-only, with both glyphs mounted at once and CSS showing the one the
2402
- // state calls for — swapping a single glyph would leave a host that slotted
2403
- // its own Send mark holding a stop icon mid-run.
2404
- this.#send.className = "send";
2405
- this.#send.type = "button";
2406
- this.#send.setAttribute("part", "send");
2407
- this.#send.replaceChildren(
2408
- glyphSlot("icon-send", "send-send", ICON_SEND),
2409
- glyphSlot("icon-stop", "send-stop", ICON_STOP),
2410
- );
2411
- this.#send.title = this.#strings.send;
2412
- this.#send.setAttribute("aria-label", this.#strings.send);
2413
- this.#send.dataset["state"] = "idle";
2414
- this.#send.addEventListener(
2415
- "click",
2416
- () => {
2318
+ const inputRow = buildComposer({
2319
+ input: this.#input,
2320
+ send: this.#send,
2321
+ attachButton: this.#attachButton,
2322
+ fileInput: this.#fileInput,
2323
+ voiceSlot: this.#voiceSlot,
2324
+ hint: this.#composerHint,
2325
+ queuedRow: this.#queuedRow,
2326
+ attachSlot: this.#attachSlot,
2327
+ strings: this.#strings,
2328
+ signal,
2329
+ onKeydown: (event) => this.#onKeydown(event),
2330
+ onInput: () => this.#onInput(),
2331
+ onSend: () => {
2417
2332
  // One button, two states: Send while idle, Stop while a run is in
2418
2333
  // flight (no layout change).
2419
2334
  if (this.#running) {
@@ -2422,53 +2337,21 @@ export class AgUiChat extends HTMLElement {
2422
2337
  }
2423
2338
  void this.#submit();
2424
2339
  },
2425
- { signal },
2426
- );
2427
-
2428
- this.#composerHint.className = "skill-hint";
2429
- this.#composerHint.setAttribute("part", "skill-hint");
2430
- this.#composerHint.hidden = true;
2431
-
2432
- // File-upload affordance: a paperclip button (hidden until
2433
- // `data-attachments-url` is wired) opening a hidden multi-file input.
2434
- // Drag-and-drop covers the whole shell (wired by ComposerAttachments).
2435
- this.#attachButton.className = "attach-btn";
2436
- this.#attachButton.type = "button";
2437
- this.#attachButton.setAttribute("part", "attach-button");
2438
- this.#attachButton.replaceChildren(glyphSlot("icon-attach", "attach-glyph", ICON_ATTACH));
2439
- this.#attachButton.title = this.#strings.attachFiles;
2440
- this.#attachButton.setAttribute("aria-label", this.#strings.attachFiles);
2441
- this.#attachButton.hidden = true;
2442
- this.#attachButton.addEventListener("click", () => this.#fileInput.click(), { signal });
2443
-
2444
- this.#fileInput.className = "attach-input";
2445
- this.#fileInput.type = "file";
2446
- this.#fileInput.multiple = true;
2447
- this.#fileInput.hidden = true;
2448
- this.#fileInput.addEventListener("change", () => this.#attachments.onFilesPicked(), {
2449
- signal,
2340
+ onFilesPicked: () => this.#attachments.onFilesPicked(),
2450
2341
  });
2451
2342
 
2452
- this.#attachSlot.className = "attachment-slot";
2453
-
2454
- // Mic button mount point (kept empty until ComposerVoice mounts the control).
2455
- this.#voiceSlot.className = "voice-slot";
2456
-
2457
2343
  // A coarse footer slot below the composer.
2458
2344
  const footer = document.createElement("slot");
2459
2345
  footer.name = "footer";
2460
2346
 
2461
- tools.append(this.#attachButton, this.#voiceSlot, this.#send);
2462
- composer.append(this.#input, tools);
2463
- inputRow.append(composer, this.#fileInput);
2464
- // Composer surfaces sit just above the input: the skills palette (opens on
2465
- // `/`), the chips, the hint saying what the composer still needs, and the
2466
- // pending-attachments tray.
2467
2347
  this.#messagesWrap.className = "messages-wrap";
2468
2348
  // Sibling of the list inside a shared box, not a child of it: the
2469
2349
  // affordance offering to scroll must not scroll away with the content.
2470
2350
  this.#messagesWrap.replaceChildren(this.#messages, this.#jumpButton, this.#excerpts.button);
2471
2351
 
2352
+ // Composer surfaces sit just above the input: the skills palette (opens on
2353
+ // `/`), the chips, the hint saying what the composer still needs, the
2354
+ // queued messages and the pending-attachments tray.
2472
2355
  this.#chat.replaceChildren(
2473
2356
  header,
2474
2357
  this.#messagesWrap,
@@ -2506,38 +2389,20 @@ export class AgUiChat extends HTMLElement {
2506
2389
  { signal },
2507
2390
  );
2508
2391
 
2509
- // What a collapsed widget shrinks to: a round floating button, or the slim
2510
- // edge rail under `placement="sidebar"` — one element, shaped by CSS.
2511
- // A sibling of the panel, so it survives the panel being hidden.
2512
- this.#launcher.className = "launcher";
2513
- this.#launcher.type = "button";
2514
- this.#launcher.setAttribute("part", "launcher");
2515
- this.#launcher.setAttribute("aria-label", this.#strings.expand);
2516
- this.#badge.className = "launcher-badge";
2517
- this.#badge.setAttribute("part", "launcher-badge");
2518
- // The count is announced through the launcher's own label, so the badge is
2519
- // decoration to a screen reader rather than a second, context-free number.
2520
- this.#badge.setAttribute("aria-hidden", "true");
2521
- this.#badge.hidden = true;
2522
- // Only the edge rail shows this. A full-height column carrying one small
2523
- // icon reads as a coloured stripe rather than a way back into a
2524
- // conversation -- it is the widest collapsed state there is and the one
2525
- // that says least about itself. Written here and hidden in CSS everywhere
2526
- // else, because the launcher is one element shaped by placement.
2527
- this.#railLabel.className = "rail-label";
2528
- this.#railLabel.setAttribute("part", "rail-label");
2529
- this.#railLabel.setAttribute("aria-hidden", "true");
2530
- this.#railLabel.textContent = this.getAttribute("title-text") ?? this.#strings.title;
2531
- this.#launcher.replaceChildren(
2532
- iconElement("launcher", "launcher-icon", ICON_LAUNCHER, readLauncherIconUrl(this)),
2533
- this.#railLabel,
2534
- this.#badge,
2535
- );
2536
- this.#launcher.addEventListener("click", () => this.setCollapsed(false), { signal });
2392
+ buildLauncher({
2393
+ launcher: this.#launcher,
2394
+ badge: this.#badge,
2395
+ railLabel: this.#railLabel,
2396
+ titleText,
2397
+ iconUrl: readLauncherIconUrl(this),
2398
+ strings: this.#strings,
2399
+ signal,
2400
+ onOpen: () => this.setCollapsed(false),
2401
+ });
2537
2402
  this.#placement.enableLauncherDrag(signal);
2538
2403
 
2539
2404
  this.#placement.mountResizeGrips(this.#chat);
2540
- adoptStyles(this.#root);
2405
+ this.#builtInSheet = adoptStyles(this.#root, this.#extraStyles);
2541
2406
  const probe = this.#placement.probe;
2542
2407
  probe.className = "viewport-probe";
2543
2408
  probe.setAttribute("aria-hidden", "true");