@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
package/src/ui/styles.ts CHANGED
@@ -1,3774 +1,104 @@
1
- // Shadow-DOM-scoped styles for the chat shell. Kept as a string constant so
2
- // the Custom Element can inject it into its shadow root without a build-time
3
- // CSS pipeline. Scoped by the shadow boundary, so class names stay terse.
4
-
5
- export const STYLES = `
6
- /* ── Token defaults ─────────────────────────────────────────────────────────
7
- Every public --ag-ui-* token is read here into a private --_* alias, and
8
- only the alias is used by the rules below.
9
-
10
- The indirection is what makes ancestor theming work. Declaring the public
11
- name on :host would set it on the host element, and a value on an element
12
- always beats one inherited from an ancestor, so tokens put on a wrapper
13
- would have no effect. Reading the public name with the default as a var()
14
- fallback leaves it undeclared on the element, so an ancestor's value
15
- inherits normally while one aimed at the element still wins.
16
-
17
- Two invariants hold this together:
18
- 1. No rule outside this file's :host blocks may reference a public name
19
- directly. A public name read outside these blocks resolves to nothing and
20
- silently drops its declaration rather than erroring, so the miss shows up
21
- as a missing colour, not a failure.
22
- 2. Every --_* alias in use is declared here. Aliases inherit like any custom
23
- property, so an undeclared one would pick up a same-named property from
24
- the host page; declaring it on :host shields the shadow tree from that. */
25
- :host {
26
- /* Colors */
27
- --_bg: var(--ag-ui-bg, #ffffff);
28
- --_fg: var(--ag-ui-fg, #1a1a2e);
29
- --_accent: var(--ag-ui-accent, #4f46e5);
30
- --_user-bg: var(--ag-ui-user-bg, #4f46e5);
31
- --_user-fg: var(--ag-ui-user-fg, #ffffff);
32
- --_assistant-bg: var(--ag-ui-assistant-bg, #f1f1f6);
33
- --_hover: var(--ag-ui-hover, #e7e7ee);
34
- --_input-bg: var(--ag-ui-input-bg, var(--_bg));
35
- --_tool-bg: var(--ag-ui-tool-bg, var(--_assistant-bg));
36
- --_tool-fg: var(--ag-ui-tool-fg, var(--_accent));
37
- --_header-bg: var(--ag-ui-header-bg, var(--_accent));
38
- --_header-fg: var(--ag-ui-header-fg, #ffffff);
39
- --_border: var(--ag-ui-border, #e2e2ec);
40
- --_radius: var(--ag-ui-radius, 12px);
41
- /* The panel's own frame, which is the one corner a placement can put against
42
- the edge of the viewport. The placements that do (page, sidebar, side, full,
43
- and all but embedded on a small viewport) square this and leave --_radius alone, so
44
- the answer well and the controls inside the panel stay as round as the
45
- theme says: none of them meets that edge. */
46
- --_panel-radius: var(--ag-ui-panel-radius, var(--_radius));
47
-
48
- /* Body text and raised chrome, read only by the code-block copy button.
49
- The defaults restate what it inherits, so a host that sets neither sees
50
- no repaint — see the note on .code-copy. */
51
- --_text: var(--ag-ui-text, var(--_fg));
52
- --_surface: var(--ag-ui-surface, transparent);
53
-
54
- /* Message action row: the control box and the mark inside it. The box has a
55
- floor of 24px so it stays a reliable target at every density. */
56
- --_action-size: var(--ag-ui-action-size, 28px);
57
- /* The header's own controls. Sized rather than left to their contents: a row
58
- of buttons each as wide as the glyph inside it comes out uneven, and uneven
59
- buttons two pixels apart read as one smudge rather than five controls. */
60
- --_header-btn-size: var(--ag-ui-header-btn-size, 30px);
61
- --_header-gap: var(--ag-ui-header-gap, 4px);
62
- --_action-icon-size: var(--ag-ui-action-icon-size, 15px);
63
- --_tooltip-bg: var(--ag-ui-tooltip-bg, #1f2430);
64
- --_tooltip-fg: var(--ag-ui-tooltip-fg, #f5f6fa);
65
-
66
- /* Text and marks drawn on top of the accent and danger fills. Separate
67
- tokens because they are not free: a host that themes the accent to a pale
68
- colour has white-on-pale with no way to correct it, which is the one
69
- theming change that makes a control unreadable rather than merely off. */
70
- --_on-accent: var(--ag-ui-on-accent, #ffffff);
71
- --_on-danger: var(--ag-ui-on-danger, #ffffff);
72
-
73
- /* Resize grips: the corner squares and the edge strips between them. Wide
74
- enough to hit without being wide enough to swallow a click on the content
75
- underneath, and adjustable for a host with a coarser pointer. */
76
- --_grip-corner: var(--ag-ui-grip-corner, 14px);
77
- --_grip-edge: var(--ag-ui-grip-edge, 6px);
78
- --_grip-edge-docked: var(--ag-ui-grip-edge-docked, 8px);
79
- /* The mark drawn inside a grip, as opposed to the area it answers to. */
80
- --_grip-mark-length: var(--ag-ui-grip-mark-length, 28px);
81
- --_grip-mark-thickness: var(--ag-ui-grip-mark-thickness, 3px);
82
-
83
- /* Status accents for tool-call cards. */
84
- --_success: var(--ag-ui-success, #15803d);
85
- --_danger: var(--ag-ui-danger, #b91c1c);
86
- --_muted: var(--ag-ui-muted, #6b7280);
87
-
88
- /* Tool-call status icon glyphs (override to re-theme) + spinner speed.
89
- The pending state is the animated ring; the settled states use these. */
90
- --_tool-icon-done: var(--ag-ui-tool-icon-done, "✓");
91
- --_tool-icon-error: var(--ag-ui-tool-icon-error, "✕");
92
- --_tool-icon-declined: var(--ag-ui-tool-icon-declined, "⊘");
93
- --_tool-icon-interrupted: var(--ag-ui-tool-icon-interrupted, "◌");
94
-
95
- /* Disclosure marks on every expandable row. Tokenised for the same reason
96
- the status icons above are: a host re-theming one set and not the other
97
- ends up with two vocabularies in one transcript. */
98
- --_disclosure-collapsed: var(--ag-ui-disclosure-collapsed, "▸");
99
- --_disclosure-expanded: var(--ag-ui-disclosure-expanded, "▾");
100
- --_tool-spin-duration: var(--ag-ui-tool-spin-duration, 0.7s);
101
-
102
- /* Answer well (opt-in via data-answer-well) — boxes a whole assistant turn. */
103
- --_well-bg: var(--ag-ui-well-bg, transparent);
104
- --_well-border: var(--ag-ui-well-border, var(--_border));
105
- --_well-radius: var(--ag-ui-well-radius, var(--_radius));
106
-
107
- /* Surface — set --ag-ui-shadow: none for a flush, embedded panel. */
108
- --_shadow: var(--ag-ui-shadow, 0 12px 32px rgba(20, 20, 50, 0.18));
109
- --_font: var(--ag-ui-font, inherit);
110
- --_font-size: var(--ag-ui-font-size, 14px);
111
- --_code-font: var(--ag-ui-code-font, ui-monospace, "SF Mono", Menlo, monospace);
112
-
113
- /* Header / launcher icon box. */
114
- --_icon-size: var(--ag-ui-icon-size, 22px);
115
- --_icon-radius: var(--ag-ui-icon-radius, 4px);
116
-
117
- /* Composer — one bordered surface holding the field and its tool row. */
118
- --_composer-radius: var(--ag-ui-composer-radius, 14px);
119
- --_composer-max-height: var(--ag-ui-composer-max-height, 40vh);
120
- --_tool-btn-size: var(--ag-ui-tool-btn-size, 30px);
121
- --_send-size: var(--ag-ui-send-size, 30px);
122
- --_glyph-size: var(--ag-ui-glyph-size, 18px);
123
- --_glyph-stroke: var(--ag-ui-glyph-stroke, 1.75);
124
-
125
- /* How wide a chart is allowed to get. A cap rather than a width: below it
126
- the chart fills its column, and above it a wider panel is just a wider
127
- panel. */
128
- --_chart-max-width: var(--ag-ui-chart-max-width, 480px);
129
-
130
- /* Unread badge on the launcher. */
131
- --_badge-bg: var(--ag-ui-badge-bg, var(--_danger));
132
- --_badge-fg: var(--ag-ui-badge-fg, #ffffff);
133
- --_badge-size: var(--ag-ui-badge-size, 18px);
134
- --_badge-font-size: var(--ag-ui-badge-font-size, 11px);
135
-
136
- /* The floating launcher a collapsed widget shrinks to. */
137
- --_launcher-size: var(--ag-ui-launcher-size, 56px);
138
- --_launcher-bg: var(--ag-ui-launcher-bg, var(--_header-bg));
139
- --_launcher-fg: var(--ag-ui-launcher-fg, var(--_header-fg));
140
- --_launcher-radius: var(--ag-ui-launcher-radius, 50%);
141
- --_launcher-icon-size: var(--ag-ui-launcher-icon-size, 26px);
142
- --_launcher-inset: var(--ag-ui-launcher-inset, auto 0 0 auto);
143
-
144
- /* Motion. One duration and two curves drive every collapse, expand and
145
- slide-over, so the widget accelerates and settles as one thing. The
146
- default curve decelerates into place; the pop curve overshoots slightly,
147
- for something arriving. */
148
- --_motion: var(--ag-ui-motion, 0.28s);
149
- --_ease: var(--ag-ui-ease, cubic-bezier(0.32, 0.72, 0, 1));
150
- --_ease-pop: var(--ag-ui-ease-pop, cubic-bezier(0.34, 1.36, 0.64, 1));
151
-
152
- /* Spacing — the density preset overrides these. */
153
- --_space: var(--ag-ui-space, 10px);
154
- --_pad: var(--ag-ui-pad, 16px);
155
- --_msg-pad: var(--ag-ui-msg-pad, 8px 12px);
156
- --_msg-radius: var(--ag-ui-msg-radius, 14px);
157
-
158
- /* Edges of the viewport the host has already spent, and that a fixed
159
- placement must therefore stay out of: a sticky nav bar, a docked toolbar,
160
- a device's safe area. Four longhands rather than one shorthand because a
161
- custom property is a token stream and CSS cannot index one -- the height
162
- arithmetic below needs the vertical pair on its own.
163
-
164
- Every fixed placement derives from these, and the heights subtract them in
165
- one place. A host that reserved its chrome by restating --ag-ui-inset per
166
- placement had to keep --ag-ui-height in step by hand, and forgetting it
167
- overflowed the panel past the bottom of the screen with nothing to say so.
168
-
169
- These take env(safe-area-inset-*) verbatim, which is what a full-bleed
170
- placement wants on a device with a notch. */
171
- --_viewport-inset-top: var(--ag-ui-viewport-inset-top, 0px);
172
- --_viewport-inset-right: var(--ag-ui-viewport-inset-right, 0px);
173
- --_viewport-inset-bottom: var(--ag-ui-viewport-inset-bottom, 0px);
174
- --_viewport-inset-left: var(--ag-ui-viewport-inset-left, 0px);
175
- /* What is left of the viewport once the host's chrome is taken out, on both
176
- axes, so a placement never composes this itself and no host can subtract
177
- an edge from the position and forget it in the height.
178
-
179
- Settable in their own right, and that is not only convention: an on-screen
180
- keyboard changes no viewport-percentage length -- not vh, not dvh, not svh
181
- -- so a full-bleed panel on a phone has to be told the height rather than
182
- deriving it. The value to publish there is the visual viewport's. */
183
- --_viewport-height: var(
184
- --ag-ui-viewport-height,
185
- calc(
186
- min(var(--_visual-viewport-inset-top) + var(--_visual-viewport-height), 100dvh - var(--_viewport-inset-bottom)) -
187
- var(--_viewport-inset-top) - var(--_keyboard-inset-top)
188
- )
189
- );
190
- /* That default is the host's box cut to the part of the screen the user can
191
- see: from the top the panel starts at, the host's reserved top moved down
192
- by the keyboard inset below, to whichever is further up, the bottom of the
193
- visible area or the host's reserved bottom. Where nothing has measured, the
194
- visible area is the layout viewport with no pan, and this is the host's
195
- box exactly.
196
-
197
- Cut rather than replaced. A measured height used as the whole answer kept
198
- a bar the host reserved at the top in the position and out of the height,
199
- so the panel ran that far past the bottom of the visible area, with its
200
- composer behind the keyboard. */
201
- /* The measured height of the part of the screen the user can actually see,
202
- written by the element from the visual viewport and falling back to the
203
- layout viewport where nothing has measured yet.
204
-
205
- This is the on-screen keyboard, and it needs measuring because no CSS
206
- length describes it: an on-screen keyboard has no effect on any
207
- viewport-percentage unit, so 100vh, 100dvh and 100svh are all the same
208
- number with the keyboard up as without it. A full-bleed panel sized from
209
- any of them puts its composer behind the keyboard the user is typing into.
210
-
211
- Separate from the token above so a host that states the usable height
212
- outright still wins: the element writes this one inline, and an inline
213
- value would otherwise outrank the host's own rule.
214
-
215
- The fallback is dvh rather than vh because the browser's own bars do move
216
- it, where a keyboard does not. iOS Safari resolves vh to the viewport with
217
- its bars collapsed, so on a page that does not scroll them away a panel
218
- sized from it ran under the address bar by the bars' height -- 40px on
219
- the phone measured -- with a docked composer underneath. dvh is the
220
- viewport with the bars as they are. */
221
- --_visual-viewport-height: var(--ag-ui-visual-viewport-height, 100dvh);
222
- /* How much of the layout viewport is hidden below the visible one, measured
223
- and written by the element alongside the height above.
224
-
225
- A shorter panel is not enough on its own for anything anchored to the
226
- bottom. A floating widget is positioned against the layout viewport, so
227
- with a keyboard up its bottom edge -- and the launcher that lives at that
228
- corner -- sits behind the keyboard however tall the panel is. This is what
229
- lifts it clear. */
230
- /* Two tokens, for the same reason the height above has two: the element
231
- writes the measurement inline, and a host that states its own value needs
232
- a knob that outranks that write rather than one the next write replaces.
233
- A host wanting no keyboard lift at all sets --ag-ui-keyboard-inset: 0px. */
234
- --_keyboard-inset: var(--ag-ui-keyboard-inset, var(--_visual-viewport-inset-bottom));
235
- --_visual-viewport-inset-bottom: var(--ag-ui-visual-viewport-inset-bottom, 0px);
236
- /* How far a panel anchored at the top moves down for a keyboard, below the
237
- top the host reserved. With the same two-token shape as the lift above;
238
- 0px keeps the panel at the host's top.
239
-
240
- To show the field being typed into, a browser pans the visible area down
241
- the layout viewport, and a fixed element stays against the layout top, so
242
- a panel that did not move showed only its lower part, from the pan down,
243
- with its header off the screen and an empty band under it. It moves by
244
- what the pan goes past the host's reserved top, not by the whole pan: the
245
- pan scrolls a bar reserved there away with the rest of the page rather
246
- than pushing it down, and adding the two put the panel a whole bar below
247
- the visible area. A corner panel anchored at the bottom does not use this,
248
- because the band below already accounts for the pan. */
249
- --_keyboard-inset-top: var(--ag-ui-keyboard-inset-top, max(0px, var(--_visual-viewport-inset-top) - var(--_viewport-inset-top)));
250
- /* How much of the layout viewport is hidden above the visible one: how far
251
- the browser panned. Measured and written by the element with the two
252
- above. */
253
- --_visual-viewport-inset-top: var(--ag-ui-visual-viewport-inset-top, 0px);
254
- --_viewport-width: var(
255
- --ag-ui-viewport-width,
256
- calc(100vw - var(--_viewport-inset-left) - var(--_viewport-inset-right))
257
- );
258
-
259
- /* Layout — override from outside to dock the widget anywhere.
260
- Set --ag-ui-position: static (and place this element in your own
261
- grid/flex layout) to embed it in the page flow instead of floating. */
262
- --_position: var(--ag-ui-position, fixed);
263
- --_z-index: var(--ag-ui-z-index, 2147483000);
264
- --_width: var(--ag-ui-width, 380px);
265
- --_height: var(--ag-ui-height, 560px);
266
- /* The gutter a resting floating panel keeps between itself and the edge of
267
- the box the host left free. One number, because the inset below spends it
268
- and the cap beneath has to know it was spent. */
269
- --_edge-gutter: var(--ag-ui-edge-gutter, 24px);
270
- --_inset: var(
271
- --ag-ui-inset,
272
- auto calc(var(--_edge-gutter) + var(--_viewport-inset-right))
273
- calc(var(--_edge-gutter) + var(--_viewport-inset-bottom) + var(--_keyboard-inset)) auto
274
- );
275
- /* The cap is what the host left free, less the *one* gutter the inset above
276
- spends on the anchored edge -- not two, and not none.
277
-
278
- Two was the first answer and it was felt on one axis only. The arithmetic
279
- says why: the default panel is 560 tall against a cap of the viewport
280
- minus 48, which on an 800px screen with a header reserved is 72px of
281
- headroom, so the height reaches its cap almost immediately; the default
282
- width is 380 against a cap near 1230, which is 850px nothing ever reaches.
283
- Once the size is capped a grip cannot grow the panel, so a pull on the
284
- anchored edge is written as position instead and the panel travels, and a
285
- pull on the free edge stops a whole gutter short of an edge a drag can
286
- reach.
287
-
288
- None was the correction, and it overshot: the panel is anchored bottom-
289
- right with a gutter already spent there, so a cap of the full usable
290
- height puts the far edge exactly one gutter outside it. On an 800px screen
291
- with nothing reserved that is a top of -24 -- the header, and every
292
- control in it, off the top of the window. Reserve a 120px header and it
293
- sits inside that instead, which is the one thing the reservation exists to
294
- prevent.
295
-
296
- One is the fixed point of both complaints. A resting panel grown to the
297
- cap runs from the usable near edge to its gutter on the far one, so the
298
- near edge is reachable by a resize exactly as it is by a drag, and neither
299
- can put any part of the panel outside the box. */
300
- --_max-width: var(--ag-ui-max-width, calc(var(--_viewport-width) - var(--_edge-gutter)));
301
- --_max-height: var(--ag-ui-max-height, calc(var(--_viewport-height) - var(--_edge-gutter)));
302
- /* Reading-column width for placement="page" (full-bleed, centred content). */
303
- --_content-max-width: var(--ag-ui-content-max-width, 820px);
304
- /* The greeting over an empty full-page conversation. The size scales with
305
- the viewport between a phone and a desktop rather than stepping at a
306
- breakpoint. */
307
- --_greeting-font: var(--ag-ui-greeting-font, var(--_font));
308
- --_greeting-size: var(--ag-ui-greeting-size, clamp(1.5rem, 4vw, 2.25rem));
309
- /* Slim rail the sidebar placement collapses to. Only that placement reads
310
- it, but it is declared here so every alias has a default in one place. */
311
- --_rail-width: var(--ag-ui-rail-width, 52px);
312
-
313
- position: var(--_position);
314
- inset: var(--_inset);
315
- z-index: var(--_z-index);
316
- width: var(--_width);
317
- max-width: var(--_max-width);
318
- height: var(--_height);
319
- max-height: var(--_max-height);
320
- display: flex;
321
- font-family: var(--_font);
322
- font-size: var(--_font-size);
323
- color: var(--_fg);
324
- }
325
-
326
- /* ── Themes ─────────────────────────────────────────────────────────────
327
- Themes only re-set the colour variables; layout/spacing are unaffected.
328
- theme="auto" follows the OS via prefers-color-scheme. */
329
- :host([theme="dark"]) {
330
- --_bg: var(--ag-ui-bg, #15151f);
331
- --_fg: var(--ag-ui-fg, #e8e8f2);
332
- --_assistant-bg: var(--ag-ui-assistant-bg, #25253a);
333
- --_hover: var(--ag-ui-hover, #303049);
334
- --_header-bg: var(--ag-ui-header-bg, #1f1f30);
335
- --_header-fg: var(--ag-ui-header-fg, #e8e8f2);
336
- --_border: var(--ag-ui-border, #33334a);
337
- --_muted: var(--ag-ui-muted, #9aa0b4);
338
- --_shadow: var(--ag-ui-shadow, 0 12px 32px rgba(0, 0, 0, 0.5));
339
- }
340
-
341
- @media (prefers-color-scheme: dark) {
342
- :host([theme="auto"]) {
343
- --_bg: var(--ag-ui-bg, #15151f);
344
- --_fg: var(--ag-ui-fg, #e8e8f2);
345
- --_assistant-bg: var(--ag-ui-assistant-bg, #25253a);
346
- --_hover: var(--ag-ui-hover, #303049);
347
- --_header-bg: var(--ag-ui-header-bg, #1f1f30);
348
- --_header-fg: var(--ag-ui-header-fg, #e8e8f2);
349
- --_border: var(--ag-ui-border, #33334a);
350
- --_muted: var(--ag-ui-muted, #9aa0b4);
351
- --_shadow: var(--ag-ui-shadow, 0 12px 32px rgba(0, 0, 0, 0.5));
352
- }
353
- }
354
-
355
- /* A terminal-flavoured "code" theme: dark, monospace, green accent. */
356
- :host([theme="code"]) {
357
- --_bg: var(--ag-ui-bg, #0d1117);
358
- --_fg: var(--ag-ui-fg, #c9d1d9);
359
- --_accent: var(--ag-ui-accent, #3fb950);
360
- --_user-bg: var(--ag-ui-user-bg, #238636);
361
- --_assistant-bg: var(--ag-ui-assistant-bg, #161b22);
362
- --_hover: var(--ag-ui-hover, #21262d);
363
- --_header-bg: var(--ag-ui-header-bg, #010409);
364
- --_header-fg: var(--ag-ui-header-fg, #c9d1d9);
365
- --_border: var(--ag-ui-border, #30363d);
366
- --_muted: var(--ag-ui-muted, #8b949e);
367
- --_font: var(--ag-ui-font, var(--_code-font));
368
- --_shadow: var(--ag-ui-shadow, 0 12px 32px rgba(0, 0, 0, 0.6));
369
- }
370
-
371
- /* ── Density ────────────────────────────────────────────────────────────── */
372
- :host([density="compact"]) {
373
- --_font-size: var(--ag-ui-font-size, 13px);
374
- --_space: var(--ag-ui-space, 6px);
375
- --_pad: var(--ag-ui-pad, 10px);
376
- --_msg-pad: var(--ag-ui-msg-pad, 5px 9px);
377
- --_msg-radius: var(--ag-ui-msg-radius, 10px);
378
- }
379
-
380
- /* ── Placement presets ──────────────────────────────────────────────────── */
381
- :host([placement="bottom-left"]) {
382
- --_inset: var(
383
- --ag-ui-inset,
384
- auto auto calc(var(--_edge-gutter) + var(--_viewport-inset-bottom) + var(--_keyboard-inset))
385
- calc(var(--_edge-gutter) + var(--_viewport-inset-left))
386
- );
387
- }
388
-
389
- :host([placement="side"]) {
390
- --_inset: var(--ag-ui-inset, calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right) var(--_viewport-inset-bottom) auto);
391
- --_width: var(--ag-ui-width, 420px);
392
- --_height: var(--ag-ui-height, var(--_viewport-height));
393
- --_max-height: var(--ag-ui-max-height, var(--_viewport-height));
394
- --_panel-radius: var(--ag-ui-panel-radius, 0);
395
- }
396
-
397
- :host([placement="full"]) {
398
- --_inset: var(
399
- --ag-ui-inset,
400
- calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right)
401
- var(--_viewport-inset-bottom) var(--_viewport-inset-left)
402
- );
403
- --_width: var(--ag-ui-width, var(--_viewport-width));
404
- --_height: var(--ag-ui-height, var(--_viewport-height));
405
- --_max-width: var(--ag-ui-max-width, var(--_viewport-width));
406
- --_max-height: var(--ag-ui-max-height, var(--_viewport-height));
407
- --_panel-radius: var(--ag-ui-panel-radius, 0);
408
- }
409
-
410
- /* Page: full-bleed background with a centred reading column capped at
411
- --ag-ui-content-max-width, where "full" is edge-to-edge and left-aligned.
412
- The column comes from symmetric auto padding on the scroll area and
413
- composer rather than a per-row wrapper, so user pills still right-align and
414
- the assistant well spans the column. */
415
- :host([placement="page"]) {
416
- --_inset: var(
417
- --ag-ui-inset,
418
- calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right)
419
- var(--_viewport-inset-bottom) var(--_viewport-inset-left)
420
- );
421
- --_width: var(--ag-ui-width, var(--_viewport-width));
422
- --_height: var(--ag-ui-height, var(--_viewport-height));
423
- --_max-width: var(--ag-ui-max-width, var(--_viewport-width));
424
- --_max-height: var(--ag-ui-max-height, var(--_viewport-height));
425
- --_panel-radius: var(--ag-ui-panel-radius, 0);
426
- }
427
-
428
- :host([placement="page"]) .messages {
429
- padding-inline: max(var(--_pad), calc((100% - var(--_content-max-width)) / 2));
430
- }
431
-
432
- :host([placement="page"]) .input-row {
433
- padding-inline: max(12px, calc((100% - var(--_content-max-width)) / 2));
434
- }
435
-
436
- /* The rows between the message list and the composer line up with the column
437
- too. Chips and tray are padding-based while palette and hint are
438
- margin-based, so each gets its own inline axis nudged by the same gutter. */
439
- :host([placement="page"]) .skill-chips,
440
- :host([placement="page"]) .attachment-tray {
441
- padding-inline: max(12px, calc((100% - var(--_content-max-width)) / 2));
442
- }
443
-
444
- :host([placement="page"]) .skill-palette,
445
- :host([placement="page"]) .skill-hint {
446
- margin-inline: max(12px, calc((100% - var(--_content-max-width)) / 2));
447
- }
448
-
449
- /* In the reading column the assistant well uses the full width; the user
450
- message stays a right-aligned pill (its default align-self + max-width). */
451
- :host([placement="page"]) .message--assistant {
452
- max-width: 100%;
453
- }
454
-
455
- /* Small viewports: one shape, reached from whichever placement the host chose.
456
-
457
- A phone is not an eighth placement, it is an override that collapses the
458
- others onto one of them. The host picked a placement for the desktop it was
459
- designing; a 380x560 panel with a 24px margin is not a smaller version of
460
- that decision, it is most of the screen with a frame drawn round it.
461
-
462
- The breakpoint is a literal because a custom property cannot be read in a
463
- media query -- which is exactly why there is an opt-out. Everything the block
464
- sets is a token a host can re-state, but the *trigger* is unreachable, so a
465
- host whose layout wants a different threshold, or none, sets
466
- data-small-viewport="off" and keeps its desktop shape at every width. 600px is above every common phone in portrait and below every
467
- tablet in landscape, and it is a width rather than a pointer test on purpose:
468
- a touch laptop is coarse-pointered and wide, a narrow desktop window is
469
- fine-pointered and small, and conflating the two gets both wrong. What the
470
- pointer decides is which controls make sense, further down.
471
-
472
- Two placements are left alone. "page" is already this shape. "embedded" sits
473
- in a box the host sized and placed, and taking that over would break the app
474
- shell it was embedded into -- the host is the only party that knows whether
475
- its column should become the whole screen. */
476
- @media (max-width: 600px) {
477
- :host([placement="floating"]:not([data-small-viewport="off"])),
478
- :host([placement="bottom-left"]:not([data-small-viewport="off"])),
479
- :host([placement="sidebar"]:not([data-small-viewport="off"])),
480
- :host([placement="side"]:not([data-small-viewport="off"])),
481
- :host(:not([placement]):not([data-small-viewport="off"])),
482
- :host([placement=""]:not([data-small-viewport="off"])) {
483
- --_inset: var(
484
- --ag-ui-inset,
485
- calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right)
486
- calc(var(--_viewport-inset-bottom) + var(--_keyboard-inset))
487
- var(--_viewport-inset-left)
488
- );
489
- --_width: var(--ag-ui-width, var(--_viewport-width));
490
- --_height: var(--ag-ui-height, var(--_viewport-height));
491
- --_max-width: var(--ag-ui-max-width, var(--_viewport-width));
492
- --_max-height: var(--ag-ui-max-height, var(--_viewport-height));
493
- --_panel-radius: var(--ag-ui-panel-radius, 0);
494
- --_shadow: var(--ag-ui-shadow, none);
495
- }
496
-
497
- /* Nothing to resize once the panel is the screen, and the grips would sit
498
- under the thumbs holding the phone. */
499
- :host([placement="floating"]:not([data-small-viewport="off"])) .resize-handle,
500
- :host([placement="bottom-left"]:not([data-small-viewport="off"])) .resize-handle,
501
- :host([placement="sidebar"]:not([data-small-viewport="off"])) .resize-handle,
502
- :host([placement="side"]:not([data-small-viewport="off"])) .resize-handle,
503
- :host(:not([placement]):not([data-small-viewport="off"])) .resize-handle,
504
- :host([placement=""]:not([data-small-viewport="off"])) .resize-handle {
505
- display: none;
506
- }
507
- }
508
-
509
- /* Sidebar: a full-height docked panel that slides open/closed and
510
- collapses to a slim icon rail (not the floating launcher). Docked right by
511
- default; data-side="left" docks it left. Overlay by default — set
512
- --ag-ui-position: static (and place this element in your own layout) for a
513
- host-managed push instead. */
514
- :host([placement="sidebar"]) {
515
- --_inset: var(--ag-ui-inset, calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right) var(--_viewport-inset-bottom) auto);
516
- --_width: var(--ag-ui-width, 420px);
517
- --_height: var(--ag-ui-height, var(--_viewport-height));
518
- --_max-height: var(--ag-ui-max-height, var(--_viewport-height));
519
- --_panel-radius: var(--ag-ui-panel-radius, 0);
520
- transition: width var(--_motion) var(--_ease);
521
- }
522
-
523
- :host([placement="sidebar"][data-side="left"]) {
524
- --_inset: var(
525
- --ag-ui-inset,
526
- calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) auto var(--_viewport-inset-bottom)
527
- var(--_viewport-inset-left)
528
- );
529
- }
530
-
531
- /* The docked panel is pinned to the edge it docks against rather than filling
532
- the host as a flex child. Collapsing shrinks the host to the rail width, and
533
- a flex child would be squashed to 52px on the way out instead of sliding out
534
- at full width. */
535
- /* The panel is taken out of flow so the collapse can slide it out at full
536
- width; see the note above. That makes the host its containing block, and the
537
- host is only one by accident: it is position: fixed by default. A host that
538
- takes the documented route to a pushed layout instead of an overlay sets
539
- --ag-ui-position: static, and a static element establishes nothing -- so the
540
- panel resolved against the initial containing block, landed at the document
541
- origin, and scrolled away with the page. Docked left it pinned to the
542
- document's left edge rather than the host's.
543
-
544
- It looked correct wherever it was first tried, because a full-height column
545
- at the top of an unscrolled document is exactly where those two answers
546
- coincide. Containment makes the host a containing block whatever its
547
- position, so the panel stays in the box the host was given. It is a no-op in
548
- the default case, which is already positioned. */
549
- :host([placement="sidebar"]) {
550
- contain: layout;
551
- }
552
-
553
- :host([placement="sidebar"]) .chat {
554
- position: absolute;
555
- inset: 0 0 0 auto;
556
- width: var(--_width);
557
- transform-origin: right center;
558
- }
559
-
560
- :host([placement="sidebar"][data-side="left"]) .chat {
561
- inset: 0 auto 0 0;
562
- transform-origin: left center;
563
- }
564
-
565
- /* Collapsed sidebar: shrink the host to the rail width and slide the panel out
566
- through the edge it docks against. Higher specificity than the generic
567
- collapse rules, so it wins regardless of source order. */
568
- /* The rail is the full height of what the host left, not of the screen. It
569
- said 100vh and pinned its own bottom, which put it under any chrome the host
570
- had reserved -- and the icon lives at the top of the rail, so the one control
571
- that reopens the panel was the first thing to disappear behind a sticky
572
- header. */
573
- :host([placement="sidebar"][collapsed]) {
574
- width: var(--_rail-width);
575
- height: var(--_viewport-height);
576
- max-height: var(--_viewport-height);
577
- pointer-events: auto;
578
- }
579
-
580
- :host([placement="sidebar"][collapsed]) .chat {
581
- transform: translateX(100%);
582
- }
583
-
584
- :host([placement="sidebar"][data-side="left"][collapsed]) .chat {
585
- transform: translateX(-100%);
586
- }
587
-
588
- @media (prefers-reduced-motion: reduce) {
589
- :host([placement="sidebar"]),
590
- :host([placement="sidebar"]) .chat {
591
- transition: none;
592
- }
593
- }
594
-
595
- /* ── Launcher ───────────────────────────────────────────────────────────────
596
- What a collapsed widget shrinks to: a round floating button by default, the
597
- sidebar's slim edge rail under that placement. A sibling of the panel, so it
598
- survives the panel being hidden, and the only part of a collapsed widget
599
- that takes pointer events. */
600
- .launcher {
601
- display: flex;
602
- position: absolute;
603
- inset: var(--_launcher-inset);
604
- align-items: center;
605
- justify-content: center;
606
- width: var(--_launcher-size);
607
- height: var(--_launcher-size);
608
- padding: 0;
609
- border: none;
610
- border-radius: var(--_launcher-radius);
611
- background: var(--_launcher-bg);
612
- color: var(--_launcher-fg);
613
- box-shadow: var(--_shadow);
614
- font: inherit;
615
- cursor: pointer;
616
- pointer-events: auto;
617
- /* A touch drag on the launcher moves it; without this the page scrolls under
618
- the finger instead and the launcher never moves at all. */
619
- touch-action: none;
620
- opacity: 0;
621
- transform: scale(0.4);
622
- visibility: hidden;
623
- transition:
624
- opacity var(--_motion) var(--_ease),
625
- transform var(--_motion) var(--_ease-pop),
626
- visibility var(--_motion) var(--_ease);
627
- }
628
-
629
- /* The launcher grows out of the corner the panel shrank into. It stays laid
630
- out at rest rather than display:none, which is what lets it animate both in
631
- and out: an unrendered element has no before-change style to transition
632
- from, and one flipping display to none cannot transition at all. visibility
633
- keeps it unpaintable, untabbable and unclickable in between, so the expanded
634
- panel's own controls underneath stay reachable. */
635
- :host([collapsed]) .launcher {
636
- opacity: 1;
637
- transform: none;
638
- visibility: visible;
639
- }
640
-
641
- :host([collapsed]) .launcher:hover {
642
- transform: scale(1.06);
643
- }
644
-
645
- :host([collapsed]) .launcher:active {
646
- transform: scale(0.94);
647
- }
648
-
649
- /* A drag is a press and a hover at the same time, so both scale rules above are
650
- live throughout it. Cancelling them is what keeps the launcher the size of
651
- the thing under the pointer while it travels; the position itself comes from
652
- inset, which nothing here transitions, so it tracks the pointer exactly. */
653
- :host([collapsed]) .launcher[data-dragging] {
654
- transform: none;
655
- cursor: grabbing;
656
- }
657
-
658
- .launcher .icon-holder {
659
- width: var(--_launcher-icon-size);
660
- height: var(--_launcher-icon-size);
661
- }
662
-
663
- /* The unread badge rides the launcher's top-right corner. The ring in the
664
- widget's own background is what separates it from the launcher underneath,
665
- whatever colour the host themes either one. */
666
- .launcher-badge {
667
- position: absolute;
668
- top: -2px;
669
- right: -2px;
670
- display: inline-flex;
671
- align-items: center;
672
- justify-content: center;
673
- box-sizing: border-box;
674
- min-width: var(--_badge-size);
675
- height: var(--_badge-size);
676
- padding: 0 5px;
677
- border-radius: 999px;
678
- background: var(--_badge-bg);
679
- color: var(--_badge-fg);
680
- font-size: var(--_badge-font-size);
681
- font-weight: 600;
682
- line-height: 1;
683
- box-shadow: 0 0 0 2px var(--_bg);
684
- }
685
-
686
- .launcher-badge[hidden] {
687
- display: none;
688
- }
689
-
690
- /* The sidebar collapses to an edge rail instead: full height, square, flush
691
- against the dock. It slides in with the panel rather than popping. */
692
- /* The edge rail. It reads as the docked edge of a panel rather than a coloured
693
- stripe: the surface the panel is made of, a border on the side it docks
694
- against, and the accent kept for the icon -- a full-height slab of accent is
695
- the loudest thing on the page and says the least about itself.
696
-
697
- Content sits at the top rather than centred, because a control floating in
698
- the middle of a screen-high column has nothing to belong to. */
699
- :host([placement="sidebar"][collapsed]) .launcher {
700
- inset: 0;
701
- width: auto;
702
- height: auto;
703
- flex-direction: column;
704
- align-items: center;
705
- justify-content: flex-start;
706
- gap: 12px;
707
- padding-top: 14px;
708
- border: 0;
709
- border-inline-start: 1px solid var(--_border);
710
- border-radius: 0;
711
- background: var(--_bg);
712
- color: var(--_fg);
713
- box-shadow: none;
714
- transform: none;
715
- }
716
-
717
- :host([placement="sidebar"][data-side="left"][collapsed]) .launcher {
718
- border-inline-start: 0;
719
- border-inline-end: 1px solid var(--_border);
720
- }
721
-
722
- /* The icon keeps the accent, so there is one obvious thing to press.
723
-
724
- The glyph is sized here rather than left to fill the holder. A glyph in an
725
- icon holder takes the holder's whole box by default, which is right where the
726
- holder is only a box and wrong the moment it becomes a filled circle: the
727
- mark then runs edge to edge and reads as a square crammed into a disc. The
728
- proportion is the floating launcher's own -- a 26px glyph in a 56px bubble --
729
- so the two collapsed states look like the same widget. */
730
- :host([placement="sidebar"][collapsed]) .launcher .icon-holder {
731
- display: inline-flex;
732
- align-items: center;
733
- justify-content: center;
734
- width: 34px;
735
- height: 34px;
736
- border-radius: 50%;
737
- background: var(--_header-bg);
738
- color: var(--_header-fg);
739
- }
740
-
741
- :host([placement="sidebar"][collapsed]) .launcher .icon-holder .glyph,
742
- :host([placement="sidebar"][collapsed]) .launcher .icon-holder .icon-img {
743
- width: 16px;
744
- height: 16px;
745
- }
746
-
747
- /* Written down the rail, which is the only direction it fits. Reading upward
748
- is the convention for a right-hand edge and matches how a docked panel's
749
- label is set everywhere it appears. */
750
- .rail-label {
751
- display: none;
752
- }
753
-
754
- :host([placement="sidebar"][collapsed]) .rail-label {
755
- display: block;
756
- writing-mode: vertical-rl;
757
- transform: rotate(180deg);
758
- max-height: calc(100% - 96px);
759
- overflow: hidden;
760
- font-size: 0.9em;
761
- font-weight: 600;
762
- letter-spacing: 0.02em;
763
- white-space: nowrap;
764
- text-overflow: ellipsis;
765
- }
766
-
767
- :host([placement="sidebar"][data-side="left"][collapsed]) .rail-label {
768
- writing-mode: vertical-rl;
769
- transform: none;
770
- }
771
-
772
- /* Every transition in this file is timed by --_motion, so collapsing it to a
773
- frame is the whole reduced-motion story: states still change (and display
774
- still flips at the end of its discrete transition), nothing travels. */
775
- @media (prefers-reduced-motion: reduce) {
776
- :host {
777
- --_motion: var(--ag-ui-motion, 0.001s);
778
- }
779
- }
780
-
781
- /* Embedded: drop the floating chrome and the high z-index stacking context so
782
- the widget lives in the host's own layout (fixes overlay/z-index clashes). */
783
- :host([placement="embedded"]) {
784
- --_position: var(--ag-ui-position, static);
785
- --_width: var(--ag-ui-width, 100%);
786
- --_height: var(--ag-ui-height, 100%);
787
- --_max-width: var(--ag-ui-max-width, 100%);
788
- --_max-height: var(--ag-ui-max-height, 100%);
789
- --_shadow: var(--ag-ui-shadow, none);
790
- --_z-index: var(--ag-ui-z-index, auto);
791
- }
792
-
793
- .chat {
794
- position: relative;
795
- display: flex;
796
- flex-direction: column;
797
- flex: 1;
798
- min-height: 0;
799
- background: var(--_bg);
800
- border: 1px solid var(--_border);
801
- border-radius: var(--_panel-radius);
802
- box-shadow: var(--_shadow);
803
- overflow: hidden;
804
- }
805
-
806
- .header {
807
- display: flex;
808
- align-items: center;
809
- justify-content: space-between;
810
- gap: 8px;
811
- padding: 10px 14px;
812
- border-bottom: 1px solid var(--_border);
813
- background: var(--_header-bg);
814
- color: var(--_header-fg);
815
- }
816
-
817
- /* The header is the panel's title bar: on the placements that let the widget
818
- be moved it drags the whole thing, matching the launcher's own drag. The
819
- selector lists those placements rather than asking JS to stamp an attribute,
820
- because a cursor that appears one frame late reads as a control that only
821
- sometimes exists. touch-action keeps a finger drag here moving the panel
822
- instead of scrolling the page behind it. */
823
- :host(:not([placement])) .header,
824
- :host([placement=""]) .header,
825
- :host([placement="floating"]) .header,
826
- :host([placement="bottom-left"]) .header {
827
- cursor: move;
828
- touch-action: none;
829
- }
830
-
831
- /* A host that turned the drag off keeps a header that says so. */
832
- :host([data-launcher-drag="false"]) .header {
833
- cursor: auto;
834
- touch-action: auto;
835
- }
836
-
837
- /* :host is carried for the specificity, not the scope: the placement rules
838
- above are a host selector plus a class plus an attribute, and a bare
839
- .header[data-dragging] loses to them -- silently, because the drag still
840
- works and only the cursor is wrong. */
841
- :host .header[data-dragging] {
842
- cursor: grabbing;
843
- }
844
-
845
- /* The cursor inherits, so a control a host slots into the header would show
846
- the drag cursor over something the drag deliberately ignores. Named to
847
- match the controls panel_drag steps aside for. */
848
- .header ::slotted(:is(button, a, input, select, textarea)) {
849
- cursor: pointer;
850
- }
851
-
852
- .header-title {
853
- flex: 1;
854
- min-width: 0;
855
- font-weight: 600;
856
- overflow: hidden;
857
- text-overflow: ellipsis;
858
- white-space: nowrap;
859
- }
860
-
861
- /* Header / launcher icon holder: a slot, with a data-icon-url <img>
862
- fallback, sized via --ag-ui-icon-size. */
863
- .icon-holder {
864
- display: inline-flex;
865
- align-items: center;
866
- justify-content: center;
867
- flex: none;
868
- width: var(--_icon-size);
869
- height: var(--_icon-size);
870
- line-height: 1;
871
- }
872
-
873
- .icon-img {
874
- width: 100%;
875
- height: 100%;
876
- object-fit: contain;
877
- border-radius: var(--_icon-radius);
878
- }
879
-
880
- .header-controls {
881
- display: flex;
882
- gap: var(--_header-gap);
883
- flex: none;
884
- }
885
-
886
- .header-btn {
887
- display: inline-flex;
888
- flex: 0 0 auto;
889
- align-items: center;
890
- justify-content: center;
891
- box-sizing: border-box;
892
- width: var(--_header-btn-size);
893
- height: var(--_header-btn-size);
894
- border: none;
895
- background: transparent;
896
- color: inherit;
897
- font: inherit;
898
- line-height: 1;
899
- padding: 0;
900
- border-radius: 6px;
901
- cursor: pointer;
902
- opacity: 0.85;
903
- }
904
-
905
- .header-btn:hover {
906
- opacity: 1;
907
- background: rgba(255, 255, 255, 0.18);
908
- }
909
-
910
- /* ── Collapse ───────────────────────────────────────────────────────────────
911
- Collapsing shrinks the widget to the round floating launcher: the panel
912
- scales toward the launcher's corner and fades, the launcher pops in from the
913
- same point. Both halves are transform and opacity only, so the morph runs on
914
- the compositor and never reflows the host page.
915
-
916
- The host box keeps its expanded size, since animating it would animate
917
- layout and a dragged --ag-ui-width would fight the launcher's own size.
918
- Nothing paints there once the panel is gone, so the box only has to stop
919
- swallowing clicks: pointer events go to none and the launcher takes them.
920
-
921
- Two placements do not use it: "sidebar" slides to its rail (below), and
922
- "embedded" keeps its header bar, having no corner for a floating circle that
923
- would escape the host's layout. "page" has no collapsed state at all. */
924
- :host([collapsed]) {
925
- pointer-events: none;
926
- /* A collapsed host has to be allowed to shrink, and in the layout hosts
927
- actually use it is not. Every collapse path here works by letting the host
928
- size to its content -- the in-flow ones set height: auto, the floating one
929
- leaves only the launcher -- and a flex or grid parent whose align-items is
930
- the default stretch value overrides all of it. The panel then hides and the box
931
- it occupied stays: a header bar over several hundred pixels of nothing.
932
- Every known consumer hit this, because putting the element in a flex column
933
- beside the page content is the obvious way to embed it. */
934
- align-self: start;
935
- }
936
-
937
- :host([collapsed]) .chat {
938
- opacity: 0;
939
- transform: scale(0.94);
940
- visibility: hidden;
941
- }
942
-
943
- /* visibility keeps the panel out of the tab order and the a11y tree at rest
944
- without display:none killing the transition. It interpolates so any progress
945
- below 1 still counts as visible: the panel stays on screen for the whole
946
- collapse, flips hidden exactly at the end, and on expand is visible from the
947
- first frame. */
948
- .chat {
949
- transform-origin: bottom right;
950
- transition:
951
- opacity var(--_motion) var(--_ease),
952
- transform var(--_motion) var(--_ease),
953
- visibility var(--_motion) var(--_ease);
954
- }
955
-
956
- :host([placement="bottom-left"]) .chat {
957
- transform-origin: bottom left;
958
- }
959
-
960
- /* Once the launcher has been dragged the element places itself, and stamps the
961
- corner it chose to open away from. The morph has to start at that same
962
- corner: scaling out of the one the placement originally guessed reads as the
963
- panel leaping across the screen before it opens.
964
-
965
- Equal specificity to the placement rule above, so source order is what lets
966
- the stamped value win -- the same arrangement the resize grip uses, and the
967
- same trap. Write these with = on the whole hyphenated token; a ~= would match
968
- whitespace-separated words and so could never match at all. */
969
- :host([data-expand-corner="top-left"]) .chat {
970
- transform-origin: top left;
971
- }
972
-
973
- :host([data-expand-corner="top-right"]) .chat {
974
- transform-origin: top right;
975
- }
976
-
977
- :host([data-expand-corner="bottom-left"]) .chat {
978
- transform-origin: bottom left;
979
- }
980
-
981
- :host([data-expand-corner="bottom-right"]) .chat {
982
- transform-origin: bottom right;
983
- }
984
-
985
- /* The in-flow placement keeps the original collapse: hide the body, let the
986
- host shrink to the header bar. */
987
- :host([collapsed][placement="embedded"]) {
988
- height: auto;
989
- max-height: none;
990
- pointer-events: auto;
991
- }
992
-
993
- :host([collapsed][placement="embedded"]) .chat {
994
- opacity: 1;
995
- transform: none;
996
- visibility: visible;
997
- }
998
-
999
- /* It keeps the header bar, so the launcher must stay out of the way: an
1000
- embedded host is position: static, which would let an absolutely-positioned
1001
- circle escape the layout and land against whatever the page positions. */
1002
- :host([collapsed][placement="embedded"]) .launcher {
1003
- visibility: hidden;
1004
- opacity: 0;
1005
- }
1006
-
1007
- :host([collapsed][placement="embedded"]) .messages-wrap,
1008
- :host([collapsed][placement="embedded"]) .messages,
1009
- :host([collapsed][placement="embedded"]) .input-row,
1010
- :host([collapsed][placement="embedded"]) .skill-chips,
1011
- :host([collapsed][placement="embedded"]) .skill-palette,
1012
- :host([collapsed][placement="embedded"]) .skill-hint {
1013
- display: none;
1014
- }
1015
-
1016
- /* The page placement has no collapsed state, so it offers no control for one.
1017
- A dedicated route has no "away" to go to: shrinking it left a strip of
1018
- application chrome fixed over a route that no longer had an owner, and this
1019
- is the one placement where the launcher that would bring it back is hidden.
1020
-
1021
- Hiding the control is only half of it. The state has another way in -- the
1022
- attribute can be written straight onto the element, and a value stored under
1023
- a different placement is restored on connect -- and the JS guards that catch
1024
- those cannot see an attribute set directly. So the placement neutralises the
1025
- state here as well: with the collapse rules above no longer naming "page", an
1026
- unguarded collapsed attribute would otherwise fall through to the generic
1027
- rule that scales the panel away and drops pointer events, leaving nothing on
1028
- screen and no launcher to press. */
1029
- :host([placement="page"]) .header-btn--collapse {
1030
- display: none;
1031
- }
1032
-
1033
- :host([placement="page"][collapsed]) {
1034
- pointer-events: auto;
1035
- align-self: auto;
1036
- }
1037
-
1038
- :host([placement="page"][collapsed]) .chat {
1039
- opacity: 1;
1040
- transform: none;
1041
- visibility: visible;
1042
- }
1043
-
1044
- /* Jump-to-latest: shown only once the reader has scrolled away *and* missed
1045
- something. Anchored to the panel rather than the list so it does not scroll
1046
- with the content it is offering to scroll to. */
1047
- /* The transcript's own box, and the only one whose foot is the transcript's
1048
- foot. The panel's foot is below the composer, the chips and the footer. */
1049
- .messages-wrap {
1050
- position: relative;
1051
- flex: 1;
1052
- min-height: 0;
1053
- display: flex;
1054
- flex-direction: column;
1055
- }
1056
-
1057
- .jump-latest {
1058
- position: absolute;
1059
- left: 50%;
1060
- transform: translateX(-50%);
1061
- bottom: var(--_pad);
1062
- z-index: 2;
1063
- display: none;
1064
- align-items: center;
1065
- gap: 0.35em;
1066
- padding: 0.4em 0.9em;
1067
- border: 1px solid var(--_border);
1068
- border-radius: 999px;
1069
- /* A raised surface, not the panel's own background. Reusing --_bg made the
1070
- pill the same colour as everything behind it, leaving a 1px border and a
1071
- shadow to carry the whole affordance -- and a dark-on-dark shadow carries
1072
- nothing. --_hover is the token that already means "lifted off the panel",
1073
- and it separates in both themes without competing with the accent the send
1074
- button owns. */
1075
- background: var(--_hover);
1076
- color: var(--_text);
1077
- font: inherit;
1078
- font-size: 0.85em;
1079
- cursor: pointer;
1080
- box-shadow: 0 2px 10px rgb(0 0 0 / 0.18);
1081
- }
1082
-
1083
- .jump-latest[data-missed="true"] {
1084
- display: flex;
1085
- }
1086
-
1087
- .jump-latest:hover {
1088
- border-color: var(--_accent);
1089
- }
1090
-
1091
- /* The offer to quote a selection. Positioned in script against the transcript
1092
- box, which is the only ancestor whose top and foot are the transcript's --
1093
- the same reason .jump-latest lives here. The translate is the half the
1094
- script does not do: script sets the point the offer hangs from, CSS decides
1095
- which corner of the offer that point is. */
1096
- .quote-selection {
1097
- position: absolute;
1098
- z-index: 2;
1099
- transform: translate(-50%, -100%);
1100
- padding: 0.25em 0.7em;
1101
- border: 1px solid var(--_border);
1102
- border-radius: 999px;
1103
- background: var(--_hover);
1104
- color: var(--_text);
1105
- font: inherit;
1106
- font-size: 0.8em;
1107
- line-height: 1.6;
1108
- white-space: nowrap;
1109
- cursor: pointer;
1110
- box-shadow: 0 2px 10px rgb(0 0 0 / 0.18);
1111
- }
1112
-
1113
- /* Flipped under the selection when there was no room above it. Only the
1114
- vertical half of the translate changes: it still hangs from its own centre
1115
- horizontally. */
1116
- .quote-selection[data-below="true"] {
1117
- transform: translate(-50%, 0);
1118
- }
1119
-
1120
- .quote-selection:hover {
1121
- border-color: var(--_accent);
1122
- }
1123
-
1124
- /* Screen-reader-only status region. Off-screen rather than display:none or
1125
- visibility:hidden, both of which take the element out of the accessibility
1126
- tree entirely -- a hidden live region announces nothing at all, which is the
1127
- classic way this pattern is written wrong.
1128
-
1129
- The 1px box with clip-path, rather than width/height 0, is the shape that
1130
- survives: a zero-sized element is dropped from the tree by some engines. */
1131
- /* A used-value reader for the four viewport-inset tokens, and the only
1132
- reliable one. getComputedStyle().getPropertyValue() on an unregistered
1133
- custom property hands back the substituted token stream, not a length: it
1134
- returns "4rem" verbatim, and "calc(56px + env(safe-area-inset-top))" as
1135
- "calc(56px + 0px)" -- which parses as NaN and takes the whole inset with it.
1136
- Padding is a real property, so the same tokens come back resolved to px.
1137
-
1138
- visibility rather than display: none, because a box that generates no
1139
- layout has no used values to read. Zero-sized, absolutely positioned and
1140
- inert, so it costs nothing but the read. */
1141
- .viewport-probe {
1142
- position: absolute;
1143
- top: 0;
1144
- left: 0;
1145
- width: 0;
1146
- height: 0;
1147
- visibility: hidden;
1148
- pointer-events: none;
1149
- padding: var(--_viewport-inset-top) var(--_viewport-inset-right)
1150
- var(--_viewport-inset-bottom) var(--_viewport-inset-left);
1151
- }
1152
-
1153
- .sr-only {
1154
- position: absolute;
1155
- width: 1px;
1156
- height: 1px;
1157
- margin: -1px;
1158
- padding: 0;
1159
- border: 0;
1160
- overflow: hidden;
1161
- white-space: nowrap;
1162
- clip-path: inset(50%);
1163
- }
1164
-
1165
- .messages {
1166
- flex: 1;
1167
- overflow-y: auto;
1168
- /* Scrolling past the end of this must not scroll the page behind it. */
1169
- overscroll-behavior: contain;
1170
- /* The browser's own scroll anchoring competes with the scroller for the same
1171
- job and wins unpredictably -- it can hold the view still exactly when we
1172
- want to follow. Turned off so following is decided in one place. Safari
1173
- does not implement it, which is itself a reason not to depend on it. */
1174
- overflow-anchor: none;
1175
- padding: var(--_pad);
1176
- display: flex;
1177
- flex-direction: column;
1178
- gap: var(--_space);
1179
- }
1180
-
1181
- /* Empty-state region (slot): centred while it's the only thing in the
1182
- list, hidden as soon as a message, card, or pending indicator renders. */
1183
- .empty {
1184
- margin: auto;
1185
- text-align: center;
1186
- color: var(--_muted);
1187
- }
1188
-
1189
- .empty[hidden] {
1190
- display: none;
1191
- }
1192
-
1193
- /* The greeting layout: a greeting over an empty conversation, and the
1194
- composer centred beneath it until the conversation has something in it.
1195
-
1196
- On by default for the page placement, where a conversation is the whole
1197
- route; data-greeting="off" turns it off there. An embedded panel opts in
1198
- with any other data-greeting value. No other placement has it: a panel
1199
- opened from a launcher is already mid-task, and a greeting in a corner
1200
- panel costs space and says little.
1201
-
1202
- The greeting itself renders under every placement and is hidden here, so a
1203
- placement switch at runtime needs nothing from script. */
1204
- .greeting {
1205
- display: none;
1206
- }
1207
-
1208
- :host([placement="page"]:not([data-greeting="off"])) .greeting,
1209
- :host([placement="embedded"][data-greeting]:not([data-greeting="off"])) .greeting {
1210
- display: block;
1211
- margin-block-end: calc(var(--_space) * 2);
1212
- font-family: var(--_greeting-font);
1213
- font-size: var(--_greeting-size);
1214
- font-weight: 500;
1215
- line-height: 1.2;
1216
- color: var(--_fg);
1217
- overflow-wrap: anywhere;
1218
- }
1219
-
1220
- /* Centring is two equal flexible boxes either side of the composer rows: the
1221
- transcript above, which already grows, and this one below. Equal zero bases
1222
- make the two halves the same height whatever they hold, so the rows sit in
1223
- the middle of the panel below its header, and stay there as a draft, an
1224
- attachment tray or a hint grows them, because growth splits into both
1225
- halves at once.
1226
-
1227
- A pseudo element rather than a node, so no host layout and nothing that
1228
- counts children can see it. It exists whether or not the conversation is
1229
- empty and only its growth changes, because a box that appears and
1230
- disappears is a box nothing can ever transition.
1231
-
1232
- Only the rule that generates it names the placements. The one that grows it
1233
- needs nothing but the state: where the first rule does not apply there is no
1234
- box to grow. The box is empty, so its automatic basis is already zero, the
1235
- same as the transcript's; nothing here writes the flex shorthand, because
1236
- this selector is the more specific of the two and a shorthand would win
1237
- over the growth. */
1238
- :host([placement="page"]:not([data-greeting="off"])) .chat::after,
1239
- :host([placement="embedded"][data-greeting]:not([data-greeting="off"])) .chat::after {
1240
- content: "";
1241
- }
1242
-
1243
- /* Not while a restore is in flight: a conversation a remote store is still
1244
- fetching is more likely to have messages than not, and centring it first
1245
- would drop the composer the moment they land. */
1246
- :host([data-empty]:not([data-restoring])) .chat::after {
1247
- flex-grow: 1;
1248
- }
1249
-
1250
- /* Only the send that leaves the empty state travels. The element arms this for
1251
- that one change and disarms it whenever the transcript is cleared, so a
1252
- restored conversation, a thread switch and a new chat all snap. Growth rather
1253
- than a transform, because the rows between the transcript and the composer
1254
- move with it, and growth is continuous where a reorder is not. Reduced motion
1255
- needs no rule of its own: the motion token already collapses to a frame
1256
- there.
1257
-
1258
- The rule over the composer fades in over the same stretch. Appearing at once,
1259
- it was drawn across the page at the composer's destination while the
1260
- composer was still on its way there. */
1261
- :host([data-composer-settling]) .chat::after {
1262
- transition: flex-grow var(--_motion) var(--_ease);
1263
- }
1264
-
1265
- :host([data-composer-settling]) .input-row {
1266
- transition: border-color var(--_motion) var(--_ease);
1267
- }
1268
-
1269
- /* The greeting and whatever the host slotted sit at the foot of the upper
1270
- half, directly over the composer, rather than floating in its middle. The
1271
- region is hidden once the conversation has content, so this needs no state
1272
- of its own. */
1273
- :host([placement="page"]:not([data-greeting="off"]):not([data-restoring])) .empty,
1274
- :host([placement="embedded"][data-greeting]:not([data-greeting="off"]):not([data-restoring])) .empty {
1275
- margin: auto auto 0;
1276
- }
1277
-
1278
- /* The rule over the composer separates it from a transcript. Over nothing it
1279
- is a line drawn across the middle of the page. */
1280
- :host([placement="page"][data-empty]:not([data-greeting="off"]):not([data-restoring])) .input-row,
1281
- :host([placement="embedded"][data-greeting][data-empty]:not([data-greeting="off"]):not([data-restoring])) .input-row {
1282
- border-block-start-color: transparent;
1283
- }
1284
-
1285
- /* On a phone the composer stays at the foot and the greeting takes the room
1286
- above it, which is the other way round from everything above.
1287
-
1288
- Centring is a shape for a screen with room to spare: the composer reads as
1289
- the one thing on the page, and whichever way the rows grow there is space
1290
- left on both sides of them. A phone has no space to spare, and the keyboard
1291
- is what takes it -- opening one leaves a 426px-tall visible area with the
1292
- composer halfway up it and an empty band underneath, which is the half of
1293
- the screen a thumb is already resting on. Docked, the field sits directly
1294
- over the keyboard, where every chat on the platform puts it, and the
1295
- greeting keeps the rest.
1296
-
1297
- Both rules restate the selector they override, with the breakpoint's own
1298
- opt-out added: this is part of the shape a host keeps its desktop layout
1299
- instead of, so it goes the same way as the rest of that shape. Restating
1300
- rather than adding conditions to the originals keeps each of those rules
1301
- readable as one decision, and source order settles the pair. */
1302
- @media (max-width: 600px) {
1303
- :host([data-empty]:not([data-restoring]):not([data-small-viewport="off"])) .chat::after {
1304
- flex-grow: 0;
1305
- }
1306
-
1307
- /* The empty region fills the transcript instead of being centred in it as a
1308
- block, so the two things in it can go to different places: the prompts the
1309
- host offers to the foot, against the composer, and the greeting to the
1310
- middle of what they leave.
1311
-
1312
- A prompt chip is a way into the conversation. Against the field it starts,
1313
- it reads as one; under the greeting halfway up the panel, with the field
1314
- at the foot, it reads as decoration next to something else. Nothing here
1315
- moves them on a screen where the composer is still centred: there the
1316
- whole region hangs at the foot of the upper half, directly over the
1317
- composer, and the prompts are already against it.
1318
-
1319
- Auto block margins on the greeting are what centre it, and they take the
1320
- free space whether or not there are prompts below to leave any, so a host
1321
- that offers none gets a greeting in the middle of the transcript rather
1322
- than one clinging to the composer.
1323
- The region declares a display here, which outranks the rule collapsing a
1324
- hidden one, so it says it is not hidden itself. Nothing sets the two
1325
- apart today -- the transcript writes the hidden property and the host's
1326
- data-empty from one expression -- and that is exactly why: a rule holding
1327
- because of how a method in another file happens to be written is a rule
1328
- holding by luck. */
1329
- :host([placement="page"][data-empty]:not([data-greeting="off"]):not([data-restoring]):not([data-small-viewport="off"])) .empty:not([hidden]),
1330
- :host([placement="embedded"][data-greeting][data-empty]:not([data-greeting="off"]):not([data-restoring]):not([data-small-viewport="off"])) .empty:not([hidden]) {
1331
- margin: 0;
1332
- flex: 1;
1333
- display: flex;
1334
- flex-direction: column;
1335
- }
1336
-
1337
- :host([placement="page"][data-empty]:not([data-greeting="off"]):not([data-restoring]):not([data-small-viewport="off"])) .greeting,
1338
- :host([placement="embedded"][data-greeting][data-empty]:not([data-greeting="off"]):not([data-restoring]):not([data-small-viewport="off"])) .greeting {
1339
- margin-block: auto;
1340
- }
1341
- }
1342
-
1343
- /* Nor does the greeting paint while a restore is in flight. Hidden rather than
1344
- removed, so it keeps its place and nothing around it moves when it shows. */
1345
- :host([data-restoring]) .greeting {
1346
- visibility: hidden;
1347
- }
1348
-
1349
- /* ── Answer group / well ─────────────────────────────────────────
1350
- One .answer per assistant turn wraps the streamed text, its tool cards,
1351
- and the pending indicator so a whole answer reads (and can be boxed) as one
1352
- unit. A flex column on the message-list gap, stretched to the list width so
1353
- its children keep their own left/right alignment. data-answer-well opts into
1354
- the bordered "well"; without it the turn renders as a flat stack. */
1355
- .answer {
1356
- display: flex;
1357
- flex-direction: column;
1358
- gap: var(--_space);
1359
- align-self: stretch;
1360
- min-width: 0;
1361
- }
1362
-
1363
- :host([data-answer-well]) .answer {
1364
- padding: var(--_pad);
1365
- background: var(--_well-bg);
1366
- border: 1px solid var(--_well-border);
1367
- border-radius: var(--_well-radius);
1368
- }
1369
-
1370
- .message {
1371
- max-width: 80%;
1372
- padding: var(--_msg-pad);
1373
- border-radius: var(--_msg-radius);
1374
- line-height: 1.4;
1375
- white-space: pre-wrap;
1376
- /* overflow-wrap, not word-break, and the difference is the whole reason a
1377
- markdown table used to be unreadable.
1378
-
1379
- Both stop one long unbroken token blowing out the bubble, which is all this
1380
- is for. But word-break: break-word is the legacy spelling of "break
1381
- anywhere", and breaking anywhere drops the *min-content* width of every
1382
- descendant to one character. A table's column algorithm takes min-content
1383
- as an input, so the table always fitted max-width: 100%, the overflow-x:
1384
- auto below it never had anything to scroll, and the columns absorbed the
1385
- pressure by rendering one letter per line instead. A seven-column header
1386
- came out 162px tall.
1387
-
1388
- overflow-wrap: break-word breaks a word only when it would otherwise
1389
- overflow its line, and leaves min-content alone -- so the bubble is still
1390
- protected and the table is free to be wider than the panel and scroll. */
1391
- overflow-wrap: break-word;
1392
- }
1393
-
1394
- /* ── Incoming-text animations (data-text-animation) ─────────────────────── */
1395
- /* .message--restored (rehydrated history) is excluded: entrance animations are
1396
- for freshly-arriving messages, not the whole transcript replaying on reload.
1397
- Word mode is excluded implicitly — restored bubbles aren't wrapped. */
1398
- :host([data-text-animation="fade"]) .message--assistant:not(.message--restored) {
1399
- animation: ag-ui-msg-fade 0.25s ease both;
1400
- }
1401
-
1402
- @keyframes ag-ui-msg-fade {
1403
- from { opacity: 0; transform: translateY(4px); }
1404
- to { opacity: 1; transform: none; }
1405
- }
1406
-
1407
- .message--assistant .word {
1408
- animation: ag-ui-word-in 0.3s ease both;
1409
- animation-delay: calc(var(--ag-ui-word-index, 0) * 35ms);
1410
- }
1411
-
1412
- @keyframes ag-ui-word-in {
1413
- from { opacity: 0; }
1414
- to { opacity: 1; }
1415
- }
1416
-
1417
- @media (prefers-reduced-motion: reduce) {
1418
- :host([data-text-animation="fade"]) .message--assistant,
1419
- .message--assistant .word {
1420
- animation: none;
1421
- }
1422
- }
1423
-
1424
- .message--user {
1425
- align-self: flex-end;
1426
- background: var(--_user-bg);
1427
- color: var(--_user-fg);
1428
- border-bottom-right-radius: 4px;
1429
- }
1430
-
1431
- .message--assistant {
1432
- align-self: flex-start;
1433
- background: var(--_assistant-bg);
1434
- border-bottom-left-radius: 4px;
1435
- /* Assistant bubbles hold rendered markdown/HTML, so collapse the source
1436
- whitespace the renderer leaves between block tags. */
1437
- white-space: normal;
1438
- }
1439
-
1440
- /* Rendered-markdown elements inside an assistant bubble. */
1441
- .message--assistant > :first-child {
1442
- margin-top: 0;
1443
- }
1444
-
1445
- .message--assistant > :last-child {
1446
- margin-bottom: 0;
1447
- }
1448
-
1449
- .message--assistant p,
1450
- .message--assistant ul,
1451
- .message--assistant ol,
1452
- .message--assistant blockquote,
1453
- .message--assistant pre {
1454
- margin: 0.5em 0;
1455
- }
1456
-
1457
- .message--assistant ul,
1458
- .message--assistant ol {
1459
- padding-left: 1.3em;
1460
- }
1461
-
1462
- .message--assistant a {
1463
- color: var(--_accent);
1464
- text-decoration: underline;
1465
- }
1466
-
1467
- .message--assistant code {
1468
- font-family: ui-monospace, "SF Mono", Menlo, monospace;
1469
- font-size: 0.92em;
1470
- background: rgba(127, 127, 127, 0.16);
1471
- padding: 1px 4px;
1472
- border-radius: 4px;
1473
- }
1474
-
1475
- .message--assistant pre {
1476
- padding: 8px 10px;
1477
- overflow: auto;
1478
- /* Scrolling past the end of this must not scroll the page behind it. */
1479
- overscroll-behavior: contain;
1480
- background: var(--_bg);
1481
- border: 1px solid var(--_border);
1482
- border-radius: 6px;
1483
- }
1484
-
1485
- .message--assistant pre code {
1486
- background: none;
1487
- padding: 0;
1488
- }
1489
-
1490
- /* The copy button sits inside the block, so it scrolls with wide code rather
1491
- than floating over the bubble. Positioning is on the pre; the button only
1492
- appears once one has been attached. */
1493
- .message--assistant pre.has-copy {
1494
- position: relative;
1495
- }
1496
-
1497
- /* The only reader of --ag-ui-surface and --ag-ui-text. Their defaults are
1498
- transparent and the body foreground, which is what this button rendered as
1499
- before either token existed; changing them repaints only this control. */
1500
- .code-copy {
1501
- position: absolute;
1502
- top: 4px;
1503
- right: 4px;
1504
- padding: 2px 8px;
1505
- font: inherit;
1506
- font-size: 0.75em;
1507
- line-height: 1.6;
1508
- color: var(--_muted);
1509
- background: var(--_surface);
1510
- border: 1px solid var(--_border);
1511
- border-radius: 4px;
1512
- cursor: pointer;
1513
- opacity: 0;
1514
- transition: opacity 0.12s ease;
1515
- }
1516
-
1517
- /* Revealed on hover or keyboard focus. Focus matters as much as hover here:
1518
- hidden-until-hover is invisible to a keyboard user otherwise. */
1519
- .message--assistant pre.has-copy:hover .code-copy,
1520
- .code-copy:focus-visible {
1521
- opacity: 1;
1522
- }
1523
-
1524
- .code-copy:hover {
1525
- color: var(--_text);
1526
- background: var(--_hover);
1527
- }
1528
-
1529
- .code-copy[data-state="copied"] {
1530
- opacity: 1;
1531
- color: var(--_text);
1532
- }
1533
-
1534
- .code-copy[data-state="failed"] {
1535
- opacity: 1;
1536
- color: var(--_danger, var(--_text));
1537
- }
1538
-
1539
- .message--assistant blockquote {
1540
- padding-left: 10px;
1541
- border-left: 3px solid var(--_border);
1542
- color: var(--_muted);
1543
- }
1544
-
1545
- /* Markdown tables. table/thead/tbody/tr/th/td are all in the sanitizer's
1546
- ALLOWED_TAGS, so an agent can emit one. A wide table must scroll inside its
1547
- own box rather than stretch the message: the bubble is width-constrained, so
1548
- without this the columns either crush or push the layout sideways. */
1549
- .message--assistant table {
1550
- display: block;
1551
- width: fit-content;
1552
- max-width: 100%;
1553
- overflow-x: auto;
1554
- border-collapse: collapse;
1555
- font-size: 0.95em;
1556
- }
1557
-
1558
- .message--assistant th,
1559
- .message--assistant td {
1560
- padding: 6px 10px;
1561
- border: 1px solid var(--_border);
1562
- text-align: left;
1563
- vertical-align: top;
1564
- }
1565
-
1566
- .message--assistant th {
1567
- background: var(--_hover);
1568
- font-weight: 600;
1569
- }
1570
-
1571
- .pending {
1572
- align-self: flex-start;
1573
- display: inline-flex;
1574
- gap: 4px;
1575
- align-items: center;
1576
- padding: 12px 14px;
1577
- background: var(--_assistant-bg);
1578
- border-radius: 14px;
1579
- border-bottom-left-radius: 4px;
1580
- }
1581
-
1582
- .pending-dot {
1583
- width: 6px;
1584
- height: 6px;
1585
- border-radius: 50%;
1586
- background: var(--_muted);
1587
- animation: ag-ui-pending 1.2s infinite ease-in-out both;
1588
- }
1589
-
1590
- .pending-dot:nth-child(2) {
1591
- animation-delay: 0.16s;
1592
- }
1593
-
1594
- .pending-dot:nth-child(3) {
1595
- animation-delay: 0.32s;
1596
- }
1597
-
1598
- @keyframes ag-ui-pending {
1599
- 0%, 80%, 100% { opacity: 0.3; transform: translateY(0); }
1600
- 40% { opacity: 1; transform: translateY(-3px); }
1601
- }
1602
-
1603
- @media (prefers-reduced-motion: reduce) {
1604
- .pending-dot {
1605
- animation: none;
1606
- opacity: 0.6;
1607
- }
1608
- }
1609
-
1610
- /* ── Thoughts region ────────────────────────────────────────────
1611
- A muted, collapsible chain-of-thought at the top of the answer group: open
1612
- while the model reasons, folded once the answer text starts. */
1613
- .thoughts {
1614
- align-self: stretch;
1615
- display: flex;
1616
- flex-direction: column;
1617
- gap: 4px;
1618
- font-size: 12px;
1619
- color: var(--_muted);
1620
- }
1621
-
1622
- .thoughts-toggle {
1623
- align-self: flex-start;
1624
- border: none;
1625
- padding: 0;
1626
- background: none;
1627
- font: inherit;
1628
- font-size: 12px;
1629
- font-weight: 600;
1630
- color: var(--_muted);
1631
- cursor: pointer;
1632
- }
1633
-
1634
- .thoughts-toggle::before {
1635
- content: var(--_disclosure-expanded) " ";
1636
- }
1637
-
1638
- .thoughts-toggle[aria-expanded="false"]::before {
1639
- content: var(--_disclosure-collapsed) " ";
1640
- }
1641
-
1642
- /* A gentle pulse on the label while reasoning is still streaming. */
1643
- .thoughts[data-streaming] .thoughts-label {
1644
- animation: ag-ui-thoughts-pulse 1.4s ease-in-out infinite;
1645
- }
1646
-
1647
- @keyframes ag-ui-thoughts-pulse {
1648
- 0%, 100% { opacity: 0.55; }
1649
- 50% { opacity: 1; }
1650
- }
1651
-
1652
- .thoughts-body {
1653
- margin: 0;
1654
- padding: 4px 0 4px 10px;
1655
- border-left: 2px solid var(--_border);
1656
- max-height: 220px;
1657
- overflow: auto;
1658
- /* Scrolling past the end of this must not scroll the page behind it. */
1659
- overscroll-behavior: contain;
1660
- white-space: pre-wrap;
1661
- word-break: break-word;
1662
- font-family: inherit;
1663
- }
1664
-
1665
- .thoughts-body[hidden] {
1666
- display: none;
1667
- }
1668
-
1669
- @media (prefers-reduced-motion: reduce) {
1670
- .thoughts[data-streaming] .thoughts-label {
1671
- animation: none;
1672
- }
1673
- }
1674
-
1675
- .tool-call {
1676
- align-self: flex-start;
1677
- max-width: 80%;
1678
- box-sizing: border-box;
1679
- display: flex;
1680
- flex-direction: column;
1681
- gap: 6px;
1682
- font-size: 12px;
1683
- font-family: ui-monospace, "SF Mono", Menlo, monospace;
1684
- padding: 8px 10px;
1685
- border-radius: 8px;
1686
- background: var(--_tool-bg);
1687
- border: 1px solid var(--_border);
1688
- color: var(--_tool-fg);
1689
- }
1690
-
1691
- /* Wraps, because the name is the only flexible child and every badge the row
1692
- gains is taken out of it. An approved call adds a third fixed badge, which in
1693
- a sidebar-width panel left the name 37px and broke it mid-word. Badges drop to
1694
- their own row instead. */
1695
- .tool-call-head {
1696
- display: flex;
1697
- flex-wrap: wrap;
1698
- align-items: center;
1699
- justify-content: space-between;
1700
- gap: 8px;
1701
- }
1702
-
1703
- .tool-call-name {
1704
- /* An auto basis, and a min-width floor rather than zero: the name may shrink,
1705
- but not below something readable, so wrapping moves a badge instead of
1706
- shredding a word. Breaking anywhere still applies to a name that cannot fit
1707
- on a line of its own, which is what keeps a long unbroken tool name inside
1708
- the card. */
1709
- flex: 1 1 auto;
1710
- min-width: 6ch;
1711
- font-weight: 600;
1712
- overflow-wrap: anywhere;
1713
- }
1714
-
1715
- /* Leading status icon. Empty in the DOM — the glyph/spinner is drawn
1716
- here from the card's data-status, so it stays themeable. */
1717
- .tool-call-icon {
1718
- flex: none;
1719
- box-sizing: border-box;
1720
- display: inline-flex;
1721
- align-items: center;
1722
- justify-content: center;
1723
- width: 14px;
1724
- height: 14px;
1725
- font-size: 12px;
1726
- line-height: 1;
1727
- }
1728
-
1729
- /* Pending: a real spinning ring. Speed is tunable; reduced motion stops it. */
1730
- .tool-call[data-status="pending"] .tool-call-icon {
1731
- border: 2px solid var(--_muted);
1732
- border-top-color: transparent;
1733
- border-radius: 50%;
1734
- animation: ag-ui-tool-spin var(--_tool-spin-duration) linear infinite;
1735
- }
1736
-
1737
- @keyframes ag-ui-tool-spin {
1738
- to { transform: rotate(360deg); }
1739
- }
1740
-
1741
- /* Deferred: no spinner, because nothing is spinning. A steady accent dot, since
1742
- the state is waiting-on-you rather than an outcome. */
1743
- .tool-call[data-status="deferred"] .tool-call-icon {
1744
- border-radius: 50%;
1745
- background: var(--_accent);
1746
- }
1747
-
1748
- /* Settled: a themeable glyph coloured by outcome. */
1749
- .tool-call[data-status="done"] .tool-call-icon::before {
1750
- content: var(--_tool-icon-done);
1751
- color: var(--_success);
1752
- }
1753
-
1754
- .tool-call[data-status="error"] .tool-call-icon::before {
1755
- content: var(--_tool-icon-error);
1756
- color: var(--_danger);
1757
- }
1758
-
1759
- .tool-call[data-status="declined"] .tool-call-icon::before {
1760
- content: var(--_tool-icon-declined);
1761
- color: var(--_muted);
1762
- }
1763
-
1764
- /* Not finished: muted like declined, since nothing failed, but its own glyph,
1765
- since nobody refused it either. */
1766
- .tool-call[data-status="interrupted"] .tool-call-icon::before {
1767
- content: var(--_tool-icon-interrupted);
1768
- color: var(--_muted);
1769
- }
1770
-
1771
- @media (prefers-reduced-motion: reduce) {
1772
- .tool-call[data-status="pending"] .tool-call-icon {
1773
- animation: none;
1774
- }
1775
- }
1776
-
1777
- /* Inline display mode: the lightest card. Drop the box chrome so the status row
1778
- reads as one line of the answer; the result toggle still expands below it. */
1779
- :host([data-tool-display="inline"]) .tool-call {
1780
- max-width: 100%;
1781
- background: transparent;
1782
- border: none;
1783
- padding: 2px 0;
1784
- gap: 2px;
1785
- }
1786
-
1787
- .tool-call-status {
1788
- flex: none;
1789
- padding: 1px 8px;
1790
- border-radius: 999px;
1791
- font-size: 11px;
1792
- font-weight: 600;
1793
- background: rgba(127, 127, 127, 0.16);
1794
- color: var(--_muted);
1795
- }
1796
-
1797
- .tool-call[data-status="deferred"] .tool-call-status {
1798
- color: var(--_accent);
1799
- }
1800
-
1801
- .tool-call[data-status="done"] .tool-call-status {
1802
- color: var(--_success);
1803
- }
1804
-
1805
- .tool-call[data-status="error"] .tool-call-status {
1806
- color: var(--_danger);
1807
- }
1808
-
1809
- .tool-call[data-status="declined"] .tool-call-status,
1810
- .tool-call[data-status="interrupted"] .tool-call-status {
1811
- color: var(--_muted);
1812
- }
1813
-
1814
- .tool-call-body {
1815
- display: flex;
1816
- flex-direction: column;
1817
- gap: 6px;
1818
- }
1819
-
1820
- .tool-call-section {
1821
- display: flex;
1822
- flex-direction: column;
1823
- gap: 3px;
1824
- }
1825
-
1826
- /* A call with no arguments drops the region rather than framing an empty
1827
- object, and a settled card drops the result region until it has one -- both
1828
- by setting the hidden property, and an author display beats the user-agent
1829
- rule for that attribute. Without this the region kept laying out: 42px of
1830
- card holding the ARGUMENTS heading over nothing, on every call the agent
1831
- made with no arguments, in the display mode that shows arguments by
1832
- default. */
1833
- .tool-call-section[hidden] {
1834
- display: none;
1835
- }
1836
-
1837
- /* The heading that tells the two payloads apart. Without it the arguments and
1838
- the result were one run of text and a reader had to guess the boundary. */
1839
- .tool-call-section-label {
1840
- font-size: 10px;
1841
- font-weight: 700;
1842
- letter-spacing: 0.06em;
1843
- text-transform: uppercase;
1844
- color: var(--_muted);
1845
- }
1846
-
1847
- .skill-item-token {
1848
- font-family: ui-monospace, "SF Mono", Menlo, monospace;
1849
- font-size: 0.92em;
1850
- color: var(--_accent);
1851
- margin-right: 6px;
1852
- }
1853
-
1854
- /* The lasting record of a human decision on a gated call — without it an
1855
- approved call looks exactly like one that was never gated. */
1856
- .tool-call-decision {
1857
- flex: none;
1858
- font-size: 11px;
1859
- font-style: italic;
1860
- color: var(--_muted);
1861
- }
1862
-
1863
- .tool-call-args,
1864
- .tool-call-result {
1865
- margin: 0;
1866
- padding: 6px 8px;
1867
- max-height: 160px;
1868
- overflow: auto;
1869
- /* Scrolling past the end of this must not scroll the page behind it. */
1870
- overscroll-behavior: contain;
1871
- background: var(--_bg);
1872
- border: 1px solid var(--_border);
1873
- border-radius: 6px;
1874
- white-space: pre-wrap;
1875
- word-break: break-word;
1876
- color: var(--_fg);
1877
- }
1878
-
1879
- /* A region a host formatter took over, marked by the card. Preformatted
1880
- whitespace is what makes the built-in block read as written, and it is the one
1881
- thing a host cannot want: a table inherits it as mangled cell spacing, and a
1882
- sentence as line breaks nobody typed.
1883
-
1884
- Whitespace only. The card's own face, frame, padding and scroll cap stay,
1885
- because the card is one visual object -- the head row and the status pill are
1886
- monospaced too -- and a region that dropped the family would be the only part
1887
- of it wearing a different one. A host that wants that restyles the
1888
- tool-card-result part, which does not need the formatter at all. */
1889
- .tool-call-args[data-formatted],
1890
- .tool-call-result[data-formatted] {
1891
- white-space: normal;
1892
- }
1893
-
1894
- /* Display modes are pure visibility over one DOM shape, selected from the host
1895
- attribute rather than a value stamped on the card at build time, so flipping
1896
- data-tool-display re-styles cards already on screen. See ToolCallCard.
1897
-
1898
- Default (no attribute) is the full mode: arguments always visible, result
1899
- behind the toggle. */
1900
- .tool-call[data-expanded="false"] .tool-call-section--result {
1901
- display: none;
1902
- }
1903
-
1904
- /* Compact: one toggle over both regions, so a settled card is a single line
1905
- until asked. */
1906
- :host([data-tool-display="compact"]) .tool-call[data-expanded="false"] .tool-call-section {
1907
- display: none;
1908
- }
1909
-
1910
- /* Inline: the result only; the call's arguments are noise at this density. */
1911
- :host([data-tool-display="inline"]) .tool-call .tool-call-section--args {
1912
- display: none;
1913
- }
1914
-
1915
- /* Minimal: the status row and nothing else, so there is no toggle to press. */
1916
- :host([data-tool-display="minimal"]) .tool-call .tool-call-toggle,
1917
- :host([data-tool-display="minimal"]) .tool-call .tool-call-body {
1918
- display: none;
1919
- }
1920
-
1921
- /* A pending card has no result yet, and in the modes where the arguments are
1922
- hidden too there is nothing behind the toggle. Hide the control rather than
1923
- offer one that expands onto nothing. A deferred card is the same, and its
1924
- arguments are shown unconditionally by the rules below. */
1925
- .tool-call[data-status="pending"] .tool-call-toggle,
1926
- .tool-call[data-status="deferred"] .tool-call-toggle,
1927
- :host([data-tool-display="inline"]) .tool-call[data-status="pending"] .tool-call-toggle {
1928
- display: none;
1929
- }
1930
-
1931
- /* The approval prompt for a gated call, rendered inside that call's own card.
1932
- Empty on every card nobody is being asked about, so it collapses instead of
1933
- adding a gap to each one. */
1934
- .tool-call-approval:empty {
1935
- display: none;
1936
- }
1937
-
1938
- .tool-call-approval {
1939
- margin-top: 8px;
1940
- }
1941
-
1942
- /* A card that is asking a question shows what it is asking about, in every
1943
- display mode. Three gated calls of one tool ask the same words, so the
1944
- arguments are the only thing telling them apart, and a density setting must
1945
- not be able to hide the answer to "which one is this". */
1946
- :host([data-tool-display="minimal"]) .tool-call[data-status="deferred"] .tool-call-body {
1947
- display: flex;
1948
- }
1949
-
1950
- /* The arguments region only, never every section: the result region carries the
1951
- hidden attribute until a result exists, and a display value here overrides it,
1952
- framing an empty RESULT heading under the question. */
1953
- :host([data-tool-display="compact"]) .tool-call[data-status="deferred"] .tool-call-section--args,
1954
- :host([data-tool-display="inline"]) .tool-call[data-status="deferred"] .tool-call-section--args {
1955
- display: flex;
1956
- }
1957
-
1958
- /* A delegated sub-agent's progress, inside the card that delegated. Empty on
1959
- every card that delegated nothing, so it collapses rather than adding a gap
1960
- to each one -- the same shape the approval slot uses. */
1961
- .tool-call-subagent:empty {
1962
- display: none;
1963
- }
1964
-
1965
- .tool-call-subagent {
1966
- display: flex;
1967
- flex-direction: column;
1968
- gap: 4px;
1969
- }
1970
-
1971
- .subagent {
1972
- display: flex;
1973
- flex-direction: column;
1974
- gap: 4px;
1975
- min-width: 0;
1976
- }
1977
-
1978
- /* The collapsed row is the status and the expander at once, which is what keeps
1979
- a ten-step child one row until somebody opens it. Full width and left-aligned,
1980
- because it is a line of the card rather than a button on it. */
1981
- .subagent-row {
1982
- display: flex;
1983
- align-items: center;
1984
- gap: 6px;
1985
- width: 100%;
1986
- box-sizing: border-box;
1987
- padding: 2px 0;
1988
- border: none;
1989
- background: none;
1990
- font: inherit;
1991
- text-align: left;
1992
- color: var(--_muted);
1993
- cursor: pointer;
1994
- }
1995
-
1996
- /* Nothing behind the row yet -- a delegation that failed before calling
1997
- anything. Drop the affordances rather than offer a control that expands onto
1998
- an empty region, which is the refusal the card's own toggle already makes. */
1999
- .subagent-row:disabled {
2000
- cursor: default;
2001
- }
2002
-
2003
- .subagent-row::after {
2004
- content: var(--_disclosure-collapsed);
2005
- flex: none;
2006
- margin-left: auto;
2007
- color: var(--_accent);
2008
- }
2009
-
2010
- .subagent-row[aria-expanded="true"]::after {
2011
- content: var(--_disclosure-expanded);
2012
- }
2013
-
2014
- .subagent-row:disabled::after {
2015
- display: none;
2016
- }
2017
-
2018
- /* Empty in the DOM; the glyph is drawn here from the panel's data-phase, so a
2019
- host re-themes it through the same tool-icon custom properties the card uses. */
2020
- .subagent-icon {
2021
- flex: none;
2022
- box-sizing: border-box;
2023
- display: inline-flex;
2024
- align-items: center;
2025
- justify-content: center;
2026
- width: 10px;
2027
- height: 10px;
2028
- font-size: 10px;
2029
- line-height: 1;
2030
- }
2031
-
2032
- /* Anything that is not a terminal phase is the child still working. Selected by
2033
- what it is not, so a phase this client has not heard of still spins rather
2034
- than rendering as a blank. */
2035
- .subagent[data-phase]:not([data-phase="finished"]):not([data-phase="failed"]) .subagent-icon {
2036
- border: 2px solid var(--_muted);
2037
- border-top-color: transparent;
2038
- border-radius: 50%;
2039
- animation: ag-ui-tool-spin var(--_tool-spin-duration) linear infinite;
2040
- }
2041
-
2042
- .subagent[data-phase="finished"] .subagent-icon::before {
2043
- content: var(--_tool-icon-done);
2044
- color: var(--_success);
2045
- }
2046
-
2047
- .subagent[data-phase="failed"] .subagent-icon::before {
2048
- content: var(--_tool-icon-error);
2049
- color: var(--_danger);
2050
- }
2051
-
2052
- @media (prefers-reduced-motion: reduce) {
2053
- .subagent .subagent-icon {
2054
- animation: none;
2055
- }
2056
- }
2057
-
2058
- /* The server's own pre-rendered line. Shrinks and wraps rather than pushing the
2059
- chevron out of the card, which is what a fixed-width sibling in a flex row
2060
- does to a panel at sidebar width. */
2061
- .subagent-status {
2062
- flex: 1 1 auto;
2063
- min-width: 0;
2064
- overflow-wrap: anywhere;
2065
- }
2066
-
2067
- /* The child's own calls. Indented and ruled, so the nesting is visible without
2068
- a second card frame around it. */
2069
- .subagent-steps {
2070
- display: flex;
2071
- flex-direction: column;
2072
- gap: 2px;
2073
- margin-left: 4px;
2074
- padding-left: 10px;
2075
- border-left: 1px solid var(--_border);
2076
- }
2077
-
2078
- .subagent-steps[hidden] {
2079
- display: none;
2080
- }
2081
-
2082
- .subagent-step {
2083
- display: flex;
2084
- align-items: center;
2085
- gap: 6px;
2086
- min-width: 0;
2087
- color: var(--_muted);
2088
- }
2089
-
2090
- .subagent-step-icon {
2091
- flex: none;
2092
- box-sizing: border-box;
2093
- display: inline-flex;
2094
- align-items: center;
2095
- justify-content: center;
2096
- width: 8px;
2097
- height: 8px;
2098
- font-size: 9px;
2099
- line-height: 1;
2100
- }
2101
-
2102
- /* No outcome yet: the wire says null while the call is in flight, and the
2103
- absence of the attribute is how that arrives here. A hollow ring, not a
2104
- spinner -- several can be on screen at once and the row above already spins. */
2105
- .subagent-step:not([data-ok]) .subagent-step-icon {
2106
- border: 1px solid var(--_muted);
2107
- border-radius: 50%;
2108
- }
2109
-
2110
- .subagent-step[data-ok="true"] .subagent-step-icon::before {
2111
- content: var(--_tool-icon-done);
2112
- color: var(--_success);
2113
- }
2114
-
2115
- .subagent-step[data-ok="false"] .subagent-step-icon::before {
2116
- content: var(--_tool-icon-error);
2117
- color: var(--_danger);
2118
- }
2119
-
2120
- .subagent-step-name {
2121
- flex: 1 1 auto;
2122
- min-width: 0;
2123
- overflow-wrap: anywhere;
2124
- }
2125
-
2126
- .tool-call-toggle {
2127
- align-self: flex-start;
2128
- border: none;
2129
- padding: 0;
2130
- background: none;
2131
- font: inherit;
2132
- font-weight: 600;
2133
- color: var(--_accent);
2134
- cursor: pointer;
2135
- }
2136
-
2137
- .tool-call-toggle::before {
2138
- content: var(--_disclosure-collapsed) " ";
2139
- }
2140
-
2141
- .tool-call-toggle[aria-expanded="true"]::before {
2142
- content: var(--_disclosure-expanded) " ";
2143
- }
2144
-
2145
- /* Resize grips: every edge and every corner, so the panel can be dragged from
2146
- whichever side the reader is already near. Absent entirely where the
2147
- placement is full-bleed, since there is nothing to drag.
2148
-
2149
- The whole set is always laid out. Which edges the *layout* pins is not a
2150
- question the stylesheet needs to answer any more -- it decided where the one
2151
- grip went, and there is no longer one grip. The element still measures and
2152
- stamps data-resize-anchor, because it decides what a drag on a pinned edge
2153
- costs in position and which grip carries the tab stop, but no rule here
2154
- reads it. */
2155
- .resize-handle {
2156
- position: absolute;
2157
- z-index: 2;
2158
- background: transparent;
2159
- touch-action: none;
2160
- }
2161
-
2162
- .resize-handle:focus-visible {
2163
- outline: 2px solid var(--_accent);
2164
- outline-offset: -2px;
2165
- }
2166
-
2167
- /* Corners first in the file and last in the DOM, so they take the pointer
2168
- where they overlap an edge strip. */
2169
- .resize-handle--top-left,
2170
- .resize-handle--top-right,
2171
- .resize-handle--bottom-left,
2172
- .resize-handle--bottom-right {
2173
- width: var(--_grip-corner);
2174
- height: var(--_grip-corner);
2175
- }
2176
-
2177
- .resize-handle--top-left {
2178
- top: 0;
2179
- left: 0;
2180
- cursor: nwse-resize;
2181
- }
2182
-
2183
- .resize-handle--top-right {
2184
- top: 0;
2185
- right: 0;
2186
- cursor: nesw-resize;
2187
- }
2188
-
2189
- .resize-handle--bottom-left {
2190
- bottom: 0;
2191
- left: 0;
2192
- cursor: nesw-resize;
2193
- }
2194
-
2195
- .resize-handle--bottom-right {
2196
- bottom: 0;
2197
- right: 0;
2198
- cursor: nwse-resize;
2199
- }
2200
-
2201
- /* Edge strips, held clear of the corners at both ends so a corner drag is
2202
- never swallowed by the edge next to it. */
2203
- .resize-handle--left,
2204
- .resize-handle--right {
2205
- top: var(--_grip-corner);
2206
- bottom: var(--_grip-corner);
2207
- width: var(--_grip-edge);
2208
- cursor: ew-resize;
2209
- }
2210
-
2211
- .resize-handle--left {
2212
- left: 0;
2213
- }
2214
-
2215
- .resize-handle--right {
2216
- right: 0;
2217
- }
2218
-
2219
- .resize-handle--top,
2220
- .resize-handle--bottom {
2221
- left: var(--_grip-corner);
2222
- right: var(--_grip-corner);
2223
- height: var(--_grip-edge);
2224
- cursor: ns-resize;
2225
- }
2226
-
2227
- .resize-handle--top {
2228
- top: 0;
2229
- }
2230
-
2231
- .resize-handle--bottom {
2232
- bottom: 0;
2233
- }
2234
-
2235
- /* Docked: the placement owns the height, so the horizontal edges and every
2236
- corner are inert and must not advertise a drag that does nothing. The two
2237
- vertical edges remain, which is the same affordance these placements had
2238
- when there was one grip -- now on both sides, since either may be the inner
2239
- one depending on which side the rail is docked to. */
2240
- :host([placement="sidebar"]) .resize-handle--top,
2241
- :host([placement="sidebar"]) .resize-handle--bottom,
2242
- :host([placement="sidebar"]) .resize-handle--top-left,
2243
- :host([placement="sidebar"]) .resize-handle--top-right,
2244
- :host([placement="sidebar"]) .resize-handle--bottom-left,
2245
- :host([placement="sidebar"]) .resize-handle--bottom-right,
2246
- :host([placement="side"]) .resize-handle--top,
2247
- :host([placement="side"]) .resize-handle--bottom,
2248
- :host([placement="side"]) .resize-handle--top-left,
2249
- :host([placement="side"]) .resize-handle--top-right,
2250
- :host([placement="side"]) .resize-handle--bottom-left,
2251
- :host([placement="side"]) .resize-handle--bottom-right {
2252
- display: none;
2253
- }
2254
-
2255
- /* The vertical edges run the full height once no corner shares them. */
2256
- :host([placement="sidebar"]) .resize-handle--left,
2257
- :host([placement="sidebar"]) .resize-handle--right,
2258
- :host([placement="side"]) .resize-handle--left,
2259
- :host([placement="side"]) .resize-handle--right {
2260
- top: 0;
2261
- bottom: 0;
2262
- width: var(--_grip-edge-docked);
2263
- }
2264
-
2265
- /* Full-bleed: nothing to drag. */
2266
- :host([placement="full"]) .resize-handle,
2267
- :host([placement="page"]) .resize-handle {
2268
- display: none;
2269
- }
2270
-
2271
- /* The visible mark on a grip.
2272
-
2273
- Filling the whole strip was what the single corner grip did, and at 14px
2274
- square nobody ever saw it. On a strip running the length of an edge the same
2275
- fill reads as a border the panel grew -- square ended, stopping short of the
2276
- corner radius at both ends, and easily mistaken for a rendering fault rather
2277
- than for something to grab.
2278
-
2279
- So the hit area stays the full strip and the mark is a short pill centred on
2280
- it, which cannot be read as an edge of anything and never meets the radius.
2281
-
2282
- It shows on hover and focus as well as during the drag. With eight grips, a
2283
- mark that appears only once you are already dragging is a mark that never
2284
- told anyone the grips were there. */
2285
- .resize-handle::after {
2286
- content: "";
2287
- position: absolute;
2288
- top: 50%;
2289
- left: 50%;
2290
- transform: translate(-50%, -50%);
2291
- border-radius: 999px;
2292
- background: var(--_accent);
2293
- opacity: 0;
2294
- transition: opacity var(--_motion) var(--_ease);
2295
- }
2296
-
2297
- .resize-handle--top::after,
2298
- .resize-handle--bottom::after {
2299
- width: var(--_grip-mark-length);
2300
- height: var(--_grip-mark-thickness);
2301
- }
2302
-
2303
- .resize-handle--left::after,
2304
- .resize-handle--right::after {
2305
- width: var(--_grip-mark-thickness);
2306
- height: var(--_grip-mark-length);
2307
- }
2308
-
2309
- /* A corner has no length to run along, so it gets a dot instead of a pill. */
2310
- .resize-handle--top-left::after,
2311
- .resize-handle--top-right::after,
2312
- .resize-handle--bottom-left::after,
2313
- .resize-handle--bottom-right::after {
2314
- width: var(--_grip-mark-thickness);
2315
- height: var(--_grip-mark-thickness);
2316
- }
2317
-
2318
- .resize-handle:hover::after,
2319
- .resize-handle:focus-visible::after {
2320
- opacity: 0.5;
2321
- }
2322
-
2323
- /* Equal specificity to the pair above, so source order is what makes the drag
2324
- the stronger of the two states. */
2325
- .resize-handle[data-dragging]::after {
2326
- opacity: 0.9;
2327
- }
2328
-
2329
- /* ── Composer ───────────────────────────────────────────────────────────────
2330
- One surface owns the border, the background and the focus ring; the field
2331
- and its tool row sit inside it, rather than being siblings stretched to the
2332
- textarea's height. */
2333
- .input-row {
2334
- display: flex;
2335
- padding: 12px;
2336
- border-top: 1px solid var(--_border);
2337
- }
2338
-
2339
- .composer {
2340
- flex: 1;
2341
- min-width: 0;
2342
- display: flex;
2343
- flex-direction: column;
2344
- gap: 4px;
2345
- padding: 6px 6px 6px 10px;
2346
- background: var(--_input-bg);
2347
- border: 1px solid var(--_border);
2348
- border-radius: var(--_composer-radius);
2349
- transition: border-color var(--_motion) var(--_ease);
2350
- }
2351
-
2352
- .composer:focus-within {
2353
- border-color: var(--_accent);
2354
- }
2355
-
2356
- /* The field grows with its content (sized by #autoGrow) up to the ceiling,
2357
- then scrolls. border-box keeps that measurement stable: with content-box the
2358
- padding would be added to every scrollHeight read and the field would creep
2359
- taller on each keystroke. */
2360
- .input {
2361
- box-sizing: border-box;
2362
- resize: none;
2363
- max-height: var(--_composer-max-height);
2364
- overflow-y: auto;
2365
- /* Scrolling past the end of this must not scroll the page behind it. */
2366
- overscroll-behavior: contain;
2367
- padding: 6px 4px 2px;
2368
- background: transparent;
2369
- border: none;
2370
- font: inherit;
2371
- color: inherit;
2372
- outline: none;
2373
- }
2374
-
2375
- .composer-tools {
2376
- display: flex;
2377
- align-items: center;
2378
- gap: 2px;
2379
- }
2380
-
2381
- /* Icon buttons: quiet at rest, so the field is what the eye lands on. */
2382
- .attach-btn,
2383
- .voice-btn {
2384
- display: inline-flex;
2385
- align-items: center;
2386
- justify-content: center;
2387
- width: var(--_tool-btn-size);
2388
- height: var(--_tool-btn-size);
2389
- padding: 0;
2390
- border: none;
2391
- border-radius: 8px;
2392
- background: transparent;
2393
- color: var(--_muted);
2394
- font: inherit;
2395
- line-height: 1;
2396
- cursor: pointer;
2397
- transition:
2398
- background-color var(--_motion) var(--_ease),
2399
- color var(--_motion) var(--_ease);
2400
- }
2401
-
2402
- .attach-btn:hover,
2403
- .voice-btn:hover {
2404
- background: var(--_hover);
2405
- color: var(--_fg);
2406
- }
2407
-
2408
- .attach-btn:disabled,
2409
- .voice-btn:disabled {
2410
- cursor: default;
2411
- opacity: 0.6;
2412
- }
2413
-
2414
- /* The same trap the attachment tray carries a note about, two rules along: an
2415
- author display beats the UA stylesheet's rule for the hidden property, so a
2416
- button the element has explicitly hidden keeps laying out and painting. The
2417
- clip is hidden until a host supplies an upload handler or an attachments URL,
2418
- and without this it is a visible control that cannot do anything.
2419
-
2420
- The mic needs no such rule, and the asymmetry is worth knowing before adding
2421
- one: it is not hidden when unconfigured, it is never built. The voice wiring
2422
- returns before constructing the button, leaving only an empty voice slot that
2423
- is display: contents. A hidden-state rule for the mic would match nothing. */
2424
- .attach-btn[hidden] {
2425
- display: none;
2426
- }
2427
-
2428
- /* Send closes the row on the right: a circle, the only filled control in the
2429
- composer, so "the thing that acts" reads at a glance. */
2430
- .send {
2431
- margin-left: auto;
2432
- display: inline-flex;
2433
- align-items: center;
2434
- justify-content: center;
2435
- flex: none;
2436
- width: var(--_send-size);
2437
- height: var(--_send-size);
2438
- padding: 0;
2439
- border: none;
2440
- border-radius: 50%;
2441
- background: var(--_accent);
2442
- color: var(--_on-accent);
2443
- font: inherit;
2444
- line-height: 1;
2445
- cursor: pointer;
2446
- transition:
2447
- background-color var(--_motion) var(--_ease),
2448
- transform var(--_motion) var(--_ease-pop);
2449
- }
2450
-
2451
- .send:hover {
2452
- transform: scale(1.08);
2453
- }
2454
-
2455
- .send:active {
2456
- transform: scale(0.92);
2457
- }
2458
-
2459
- .send:disabled {
2460
- opacity: 0.5;
2461
- cursor: default;
2462
- transform: none;
2463
- }
2464
-
2465
- /* The composer button doubles as the Stop control while a run is in flight —
2466
- same circle, different glyph, so nothing moves when a run starts. */
2467
- .send[data-state="running"] {
2468
- background: var(--_muted);
2469
- }
2470
-
2471
- .send[data-state="idle"] .send-stop,
2472
- .send[data-state="running"] .send-send {
2473
- display: none;
2474
- }
2475
-
2476
- /* Glyphs — one class, painted from the button's own colour so every state
2477
- (hover, recording, running) carries the icon with it. */
2478
- .glyph {
2479
- width: var(--_glyph-size);
2480
- height: var(--_glyph-size);
2481
- fill: none;
2482
- stroke: currentColor;
2483
- stroke-width: var(--_glyph-stroke);
2484
- stroke-linecap: round;
2485
- stroke-linejoin: round;
2486
- }
2487
-
2488
- .glyph--solid {
2489
- fill: currentColor;
2490
- stroke: none;
2491
- }
2492
-
2493
- /* In an icon holder (header brand, launcher) the glyph takes the holder's size
2494
- rather than the composer's. */
2495
- .icon-holder .glyph {
2496
- width: 100%;
2497
- height: 100%;
2498
- }
2499
-
2500
- /* ── File attachments ───────────────────────────────────────────────────── */
2501
- /* The picker button sits in the composer's tool row; hidden until
2502
- data-attachments-url. */
2503
- .attach-input {
2504
- display: none;
2505
- }
2506
-
2507
- /* The mic button's mount point; filled only once ComposerVoice.wire mounts
2508
- the control, which it skips unless transcription is configured. */
2509
- .voice-slot {
2510
- display: contents;
2511
- }
2512
-
2513
- /* Recording: a red tint + a gentle pulse so it's clearly "live". */
2514
- .voice-btn[data-state="recording"] {
2515
- background: var(--_danger);
2516
- color: var(--_on-danger);
2517
- animation: ag-ui-voice-pulse 1.2s ease-in-out infinite;
2518
- }
2519
-
2520
- .voice-btn[data-state="recording"]:hover {
2521
- background: var(--_danger);
2522
- color: var(--_on-danger);
2523
- }
2524
-
2525
- @keyframes ag-ui-voice-pulse {
2526
- 0%, 100% { opacity: 1; }
2527
- 50% { opacity: 0.6; }
2528
- }
2529
-
2530
- @media (prefers-reduced-motion: reduce) {
2531
- .voice-btn[data-state="recording"] {
2532
- animation: none;
2533
- }
2534
- }
2535
-
2536
- /* Pending-attachments tray, above the input row; collapses (hidden) when empty. */
2537
- .attachment-slot {
2538
- display: contents;
2539
- }
2540
-
2541
- /* The padding is the only separation the tray gets: its slot is display:
2542
- contents, so the tray is a direct child of the .chat column, and that column
2543
- sets no gap. The bottom value keeps a chip off the composer's top edge.
2544
- The inline value here is only the default: the page placement overrides
2545
- padding-inline further down to compute its reading-column gutter, and wins
2546
- on specificity whichever way this declaration is written. Read the two
2547
- together before changing either. */
2548
- .attachment-tray {
2549
- display: flex;
2550
- flex-wrap: wrap;
2551
- gap: 6px;
2552
- padding: 8px 12px;
2553
- }
2554
-
2555
- /* The tray sets the hidden property while empty, and an author display beats
2556
- the UA stylesheet's rule for it, so without this the empty tray keeps
2557
- laying out and its padding is permanent dead space above the composer. */
2558
- .attachment-tray[hidden] {
2559
- display: none;
2560
- }
2561
-
2562
- .attachment-chips {
2563
- display: flex;
2564
- flex-wrap: wrap;
2565
- gap: 6px;
2566
- margin-top: 6px;
2567
- }
2568
-
2569
- /* Charts.
2570
- *
2571
- * The SVG scales to the column and carries no colours of its own beyond the
2572
- * series palette, so a host restyles it the same way it restyles everything
2573
- * else. Series colours are custom properties with fallbacks rather than fixed
2574
- * values, and the axis furniture inherits currentColor at low opacity so it
2575
- * reads correctly in either theme without a second palette.
2576
- */
2577
- /* A chart is sized like a message, not like a panel: it takes the width it
2578
- needs and stops, rather than stretching into whatever room the transcript
2579
- has. A message bubble caps at 80% for the same reason -- widening the panel
2580
- should not resize what is already in it. The cap is a token so a host with a
2581
- wide panel and a real reason can raise it; the default is the width this
2582
- renderer has always drawn at. Below the cap the block still fills its column,
2583
- which is what a narrow panel needs. */
2584
- .chart-block {
2585
- align-self: flex-start;
2586
- width: 100%;
2587
- max-width: var(--_chart-max-width);
2588
- margin: 6px 0;
2589
- color: var(--_fg);
2590
- }
2591
-
2592
- /* The drawing fills the block's width and carries its own height in the
2593
- viewBox, so it is laid out rather than magnified: block display keeps the
2594
- inline baseline gap from adding a stripe under every chart. */
2595
- .chart-block svg {
2596
- display: block;
2597
- width: 100%;
2598
- height: auto;
2599
- }
2600
-
2601
- .chart-title {
2602
- margin-bottom: 2px;
2603
- font-size: 0.85em;
2604
- font-weight: 600;
2605
- opacity: 0.85;
2606
- }
2607
-
2608
- .chart-legend {
2609
- display: flex;
2610
- flex-wrap: wrap;
2611
- gap: 4px 12px;
2612
- margin-top: 4px;
2613
- font-size: 0.78em;
2614
- opacity: 0.75;
2615
- }
2616
-
2617
- .chart-legend-item {
2618
- display: inline-flex;
2619
- align-items: center;
2620
- gap: 5px;
2621
- }
2622
-
2623
- .chart-legend-swatch {
2624
- width: 9px;
2625
- height: 9px;
2626
- border-radius: 2px;
2627
- flex: 0 0 auto;
2628
- }
2629
-
2630
- .run-notice {
2631
- display: inline-flex;
2632
- align-items: center;
2633
- gap: 6px;
2634
- align-self: flex-start;
2635
- max-width: 100%;
2636
- margin: 2px 0;
2637
- padding: 3px 10px;
2638
- border: 1px dashed var(--_border);
2639
- border-radius: 999px;
2640
- background: transparent;
2641
- color: var(--_muted);
2642
- font-size: 0.8em;
2643
- line-height: 1.4;
2644
- }
2645
-
2646
- .run-notice-icon {
2647
- flex: none;
2648
- opacity: 0.75;
2649
- }
2650
-
2651
- /* The one control a notice may carry. Quiet, because it reports something
2652
- already done rather than asking for a decision. */
2653
- .run-notice-undo {
2654
- flex: 0 0 auto;
2655
- margin-inline-start: auto;
2656
- padding: 2px 8px;
2657
- border: 1px solid var(--_border);
2658
- border-radius: var(--_radius);
2659
- background: var(--_bg);
2660
- color: var(--_accent);
2661
- font: inherit;
2662
- font-size: 0.9em;
2663
- cursor: pointer;
2664
- }
2665
-
2666
- .run-notice-undo:hover:not(:disabled) {
2667
- background: var(--_hover);
2668
- }
2669
-
2670
- .run-notice-undo:disabled {
2671
- opacity: 0.5;
2672
- cursor: default;
2673
- }
2674
-
2675
- .run-notice-text {
2676
- min-width: 0;
2677
- overflow-wrap: anywhere;
2678
- }
2679
-
2680
- /* A chip carries its own text colour because it carries its own background.
2681
- The same chip renders in two places with opposite inherited colours: the
2682
- composer tray gives it the panel's, a sent user bubble gives it the user
2683
- foreground, which is white on the stock light theme and near-invisible
2684
- against the chip. Pairing the colour with the background it belongs to makes
2685
- both placements read alike. Overridden below for an errored chip, which
2686
- keeps its red. */
2687
- .attachment-chip {
2688
- display: inline-flex;
2689
- align-items: center;
2690
- gap: 6px;
2691
- /* Without this the 100% below caps the content box, so the chip still
2692
- overflows its container by its own padding and border. Invisible while a
2693
- character cap kept names short; routine once the container is what bounds
2694
- the name. */
2695
- box-sizing: border-box;
2696
- max-width: 100%;
2697
- padding: 4px 8px;
2698
- border: 1px solid var(--_border);
2699
- border-radius: 999px;
2700
- background: var(--_assistant-bg);
2701
- color: var(--_text);
2702
- font-size: 0.85em;
2703
- position: relative;
2704
- }
2705
-
2706
- .attachment-chip--error {
2707
- border-color: var(--_danger);
2708
- color: var(--_danger);
2709
- }
2710
-
2711
- /* The type mark. Painted from currentColor, so it carries the chip's state
2712
- with it and turns red along with an errored one; muted by opacity rather
2713
- than a colour, which is what keeps that true. Sized from the chip's own text
2714
- rather than --ag-ui-glyph-size, the way an icon holder's glyph takes the
2715
- holder's size: the composer's 18px buttons would make a chip button-height. */
2716
- .attachment-chip-icon {
2717
- display: inline-flex;
2718
- flex: none;
2719
- opacity: 0.75;
2720
- }
2721
-
2722
- .attachment-chip-icon .glyph {
2723
- width: 1.25em;
2724
- height: 1.25em;
2725
- }
2726
-
2727
- /* No character cap: the chip is already max-width 100%, so its container is
2728
- what bounds the name, and a fixed cap only truncated names the chip had room
2729
- for. min-width: 0 is what lets the flex item shrink past its content, so the
2730
- ellipsis appears at the container edge instead of the chip overflowing. */
2731
- .attachment-chip-name {
2732
- overflow: hidden;
2733
- text-overflow: ellipsis;
2734
- white-space: nowrap;
2735
- min-width: 0;
2736
- }
2737
-
2738
- .attachment-chip-size {
2739
- color: var(--_muted);
2740
- white-space: nowrap;
2741
- }
2742
-
2743
- .attachment-chip--error .attachment-chip-size {
2744
- color: var(--_danger);
2745
- }
2746
-
2747
- /* The progress bar fills as the file uploads. */
2748
- .attachment-chip-bar {
2749
- flex-basis: 100%;
2750
- height: 3px;
2751
- border-radius: 2px;
2752
- background: var(--_border);
2753
- overflow: hidden;
2754
- }
2755
-
2756
- .attachment-chip-bar-fill {
2757
- height: 100%;
2758
- background: var(--_accent);
2759
- transition: width 0.15s ease;
2760
- }
2761
-
2762
- .attachment-chip-remove,
2763
- .attachment-chip-retry {
2764
- border: none;
2765
- background: none;
2766
- color: inherit;
2767
- cursor: pointer;
2768
- padding: 0;
2769
- line-height: 1;
2770
- opacity: 0.7;
2771
- }
2772
-
2773
- .attachment-chip-remove:hover,
2774
- .attachment-chip-retry:hover {
2775
- opacity: 1;
2776
- }
2777
-
2778
- /* A subtle outline while a file is dragged over the shell. */
2779
- .chat--dragover {
2780
- outline: 2px dashed var(--_accent);
2781
- outline-offset: -4px;
2782
- }
2783
-
2784
- /* Muted "⏹ Stopped" line after a cancelled run — a note, not an error bubble. */
2785
- .stopped-note {
2786
- align-self: flex-start;
2787
- color: var(--_muted);
2788
- font-size: 12px;
2789
- padding: 2px 4px;
2790
- }
2791
-
2792
- /* Inline confirmation card — lives in the transcript, no focus-stealing overlay. */
2793
- .confirm {
2794
- align-self: stretch;
2795
- box-sizing: border-box;
2796
- display: flex;
2797
- flex-direction: column;
2798
- gap: 8px;
2799
- padding: 12px;
2800
- background: var(--_bg);
2801
- border: 1px solid var(--_accent);
2802
- border-radius: 10px;
2803
- }
2804
-
2805
- .confirm[data-resolved] {
2806
- opacity: 0.7;
2807
- border-color: var(--_border);
2808
- }
2809
-
2810
- .confirm-body {
2811
- font-weight: 600;
2812
- }
2813
-
2814
- .confirm-args {
2815
- margin: 0;
2816
- padding: 8px 10px;
2817
- max-height: 140px;
2818
- overflow: auto;
2819
- /* Scrolling past the end of this must not scroll the page behind it. */
2820
- overscroll-behavior: contain;
2821
- font-size: 12px;
2822
- font-family: ui-monospace, "SF Mono", Menlo, monospace;
2823
- background: var(--_assistant-bg);
2824
- border-radius: 8px;
2825
- white-space: pre-wrap;
2826
- word-break: break-word;
2827
- }
2828
-
2829
- .confirm-actions {
2830
- display: flex;
2831
- flex-wrap: wrap;
2832
- gap: 8px;
2833
- justify-content: flex-end;
2834
- }
2835
-
2836
- .confirm-btn {
2837
- border: 1px solid var(--_border);
2838
- border-radius: 8px;
2839
- padding: 8px 14px;
2840
- font: inherit;
2841
- font-weight: 600;
2842
- cursor: pointer;
2843
- background: var(--_bg);
2844
- color: var(--_fg);
2845
- }
2846
-
2847
- .confirm-btn:disabled {
2848
- cursor: default;
2849
- opacity: 0.6;
2850
- }
2851
-
2852
- .confirm-btn--confirm {
2853
- border-color: var(--_accent);
2854
- background: var(--_accent);
2855
- color: var(--_on-accent);
2856
- }
2857
-
2858
- /* The session waiver. Deliberately the quietest of the three: it is the widest
2859
- decision on the card, so it should be reachable without being the one the eye
2860
- lands on when the user means to say yes once. */
2861
- .confirm-btn--always {
2862
- font-weight: 500;
2863
- opacity: 0.85;
2864
- }
2865
-
2866
- .confirm-btn--always:hover,
2867
- .confirm-btn--always:focus-visible {
2868
- opacity: 1;
2869
- }
2870
-
2871
- /* Editable arguments on an approval card. A plain field rather than a code
2872
- editor: it holds the JSON a card already displays, and the only interaction
2873
- is correcting a value before letting the call run. */
2874
- .approval-edit {
2875
- display: flex;
2876
- flex-direction: column;
2877
- gap: 6px;
2878
- }
2879
-
2880
- .approval-args {
2881
- box-sizing: border-box;
2882
- width: 100%;
2883
- resize: vertical;
2884
- border: 1px solid var(--_border);
2885
- border-radius: 8px;
2886
- padding: 8px;
2887
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
2888
- font-size: 0.85em;
2889
- background: var(--_bg);
2890
- color: var(--_fg);
2891
- }
2892
-
2893
- .approval-args:focus-visible {
2894
- border-color: var(--_accent);
2895
- outline: none;
2896
- }
2897
-
2898
- .approval-error {
2899
- font-size: 0.85em;
2900
- color: var(--_danger);
2901
- }
2902
-
2903
- /* Message action row. Sits under a finished assistant bubble.
2904
-
2905
- The wrap is insurance rather than a fix: these buttons are glyph-only, so at
2906
- every width tested they fit on one line and removing the wrap changes
2907
- nothing. It is here because the confirmation row one release earlier did
2908
- overflow when it gained a third button, off the left edge and outside its own
2909
- card, and the difference between the two rows is only that this one's labels
2910
- are icons today. */
2911
- .message-actions {
2912
- display: flex;
2913
- flex-wrap: wrap;
2914
- gap: 4px;
2915
- /* Negative, and that is the point. The answer group is a flex column with its
2916
- own gap, so a positive margin here pushes the row further from the message
2917
- it acts on than the next card is below it -- the buttons then read as
2918
- belonging to whatever follows. Pulling back inside the gap is what makes
2919
- them the message's own. */
2920
- margin-top: -6px;
2921
- }
2922
-
2923
- /* The row's controls are icon-only, so the box is the whole target. Sized from
2924
- a variable a host can raise, with a floor rather than a fixed height: the
2925
- compact density shrinks the font, and a target that shrinks with it lands
2926
- under the 24px minimum that makes a control reliably tappable. It used to,
2927
- at roughly 20px square. */
2928
- .message-action {
2929
- display: inline-flex;
2930
- align-items: center;
2931
- justify-content: center;
2932
- box-sizing: border-box;
2933
- min-width: var(--_action-size);
2934
- min-height: var(--_action-size);
2935
- padding: 4px;
2936
- border: none;
2937
- border-radius: 6px;
2938
- font: inherit;
2939
- line-height: 1.2;
2940
- cursor: pointer;
2941
- background: transparent;
2942
- color: var(--_muted);
2943
- opacity: 0.75;
2944
- /* The tooltip below is positioned against this box. */
2945
- position: relative;
2946
- }
2947
-
2948
- .message-action:hover,
2949
- .message-action:focus-visible {
2950
- opacity: 1;
2951
- background: var(--_border);
2952
- }
2953
-
2954
- .message-action-icon {
2955
- display: inline-flex;
2956
- width: var(--_action-icon-size);
2957
- height: var(--_action-icon-size);
2958
- }
2959
-
2960
- .message-action-icon .glyph {
2961
- width: 100%;
2962
- height: 100%;
2963
- }
2964
-
2965
- /* The label, drawn rather than left to the browser.
2966
-
2967
- A title attribute is the usual answer and covers only half the readers: it
2968
- never appears on keyboard focus, so tabbing onto an icon-only control shows
2969
- nothing at all. The attribute stays for the pointer users who expect it, and
2970
- this shows the same words on hover and on focus alike.
2971
-
2972
- Left-aligned rather than centred because the row sits at the left edge of an
2973
- answer inside a scrolling column, and a centred tooltip on the first control
2974
- is clipped by that column. Growing rightward keeps it inside.
2975
-
2976
- Not exposed to assistive technology: the button already carries the same
2977
- string as its accessible name, and this would be a second copy of it. */
2978
- .message-action::after {
2979
- content: attr(data-tooltip);
2980
- position: absolute;
2981
- bottom: calc(100% + 4px);
2982
- left: 0;
2983
- z-index: 3;
2984
- padding: 3px 6px;
2985
- border-radius: 4px;
2986
- background: var(--_tooltip-bg);
2987
- color: var(--_tooltip-fg);
2988
- font-size: 11px;
2989
- line-height: 1.4;
2990
- white-space: nowrap;
2991
- pointer-events: none;
2992
- opacity: 0;
2993
- transition: opacity var(--_motion) var(--_ease);
2994
- }
2995
-
2996
- .message-action:hover::after,
2997
- .message-action:focus-visible::after {
2998
- opacity: 1;
2999
- }
3000
-
3001
- /* Touch has no hover to reveal it, and a tooltip that latches open under a
3002
- finger covers the answer it belongs to. */
3003
- @media (hover: none) {
3004
- .message-action::after {
3005
- content: none;
3006
- }
3007
- }
3008
-
3009
- /* Touch. Separate from the width breakpoint above on purpose: width decides
3010
- the layout, the pointer decides which controls make sense, and a touch
3011
- laptop is coarse-pointered and wide while a narrow desktop window is
3012
- fine-pointered and small. */
3013
- @media (pointer: coarse) {
3014
- /* A 6px edge strip is not a control, it is a trap that eats a scroll. The
3015
- corners are 14px, still under the 24px floor a target is meant to clear,
3016
- and the panel has other ways to be resized on a device that has a pointer
3017
- precise enough to grab one. */
3018
- .resize-handle {
3019
- display: none;
3020
- }
3021
-
3022
- /* Platform guidance is 44pt on iOS and 48dp on Android. These sit at 28-30px,
3023
- which clears the WCAG minimum and misses both. */
3024
- :host {
3025
- --_action-size: var(--ag-ui-action-size, 44px);
3026
- --_tool-btn-size: var(--ag-ui-tool-btn-size, 44px);
3027
- --_send-size: var(--ag-ui-send-size, 44px);
3028
- /* Missed the first time these were raised, which left the header -- the
3029
- widget's primary controls, and the only way to reach history, a new chat
3030
- or the collapse -- at half the size of everything beside it. */
3031
- --_header-btn-size: var(--ag-ui-header-btn-size, 44px);
3032
- --_header-gap: var(--ag-ui-header-gap, 6px);
3033
- }
3034
-
3035
- /* iOS Safari zooms the page when a control under 16px takes focus, which
3036
- drags the whole fixed panel with it and leaves the user pinching back out
3037
- of a chat they only wanted to type into. The composer inherits the widget
3038
- font, which is 14px by default and 13px at compact density, so it has to
3039
- say 16 outright -- and only ever upward, so a host that has deliberately
3040
- set something larger keeps it. */
3041
- .input {
3042
- font-size: max(16px, var(--_font-size));
3043
- }
3044
- }
3045
-
3046
- .message-action[aria-pressed="true"] {
3047
- opacity: 1;
3048
- color: var(--_accent);
3049
- }
3050
-
3051
- .message-action--confirmed {
3052
- opacity: 1;
3053
- color: var(--_accent);
3054
- }
3055
-
3056
- /* Approval card — the server-side-tool gate (approve/deny an interrupt). */
3057
- .approval {
3058
- align-self: stretch;
3059
- box-sizing: border-box;
3060
- display: flex;
3061
- flex-direction: column;
3062
- gap: 8px;
3063
- padding: 12px;
3064
- background: var(--_bg);
3065
- border: 1px solid var(--_accent);
3066
- border-radius: 10px;
3067
- }
3068
-
3069
- .approval[data-resolved] {
3070
- opacity: 0.7;
3071
- border-color: var(--_border);
3072
- }
3073
-
3074
- .approval-body {
3075
- font-weight: 600;
3076
- }
3077
-
3078
- .approval-actions {
3079
- display: flex;
3080
- gap: 8px;
3081
- justify-content: flex-end;
3082
- }
3083
-
3084
- .approval-btn {
3085
- border: 1px solid var(--_border);
3086
- border-radius: 8px;
3087
- padding: 8px 14px;
3088
- font: inherit;
3089
- font-weight: 600;
3090
- cursor: pointer;
3091
- background: var(--_bg);
3092
- color: var(--_fg);
3093
- }
3094
-
3095
- .approval-btn:disabled {
3096
- cursor: default;
3097
- opacity: 0.6;
3098
- }
3099
-
3100
- .approval-btn--approve {
3101
- border-color: var(--_accent);
3102
- background: var(--_accent);
3103
- color: var(--_on-accent);
3104
- }
3105
-
3106
- /* Question card — the built-in ask_user prompt (radios and/or free text). */
3107
- .question {
3108
- align-self: stretch;
3109
- box-sizing: border-box;
3110
- display: flex;
3111
- flex-direction: column;
3112
- gap: 8px;
3113
- padding: 12px;
3114
- background: var(--_bg);
3115
- border: 1px solid var(--_accent);
3116
- border-radius: 10px;
3117
- }
3118
-
3119
- .question[data-resolved] {
3120
- opacity: 0.7;
3121
- border-color: var(--_border);
3122
- }
3123
-
3124
- .question-body {
3125
- font-weight: 600;
3126
- }
3127
-
3128
- .question-options {
3129
- display: flex;
3130
- flex-direction: column;
3131
- gap: 6px;
3132
- }
3133
-
3134
- .question-choice {
3135
- display: flex;
3136
- align-items: center;
3137
- gap: 8px;
3138
- cursor: pointer;
3139
- }
3140
-
3141
- .question-input {
3142
- box-sizing: border-box;
3143
- width: 100%;
3144
- padding: 8px 10px;
3145
- font: inherit;
3146
- color: var(--_fg);
3147
- background: var(--_bg);
3148
- border: 1px solid var(--_border);
3149
- border-radius: 8px;
3150
- }
3151
-
3152
- .question-input:disabled {
3153
- opacity: 0.6;
3154
- }
3155
-
3156
- .question-actions {
3157
- display: flex;
3158
- justify-content: flex-end;
3159
- }
3160
-
3161
- .question-btn {
3162
- border: 1px solid var(--_accent);
3163
- border-radius: 8px;
3164
- padding: 8px 14px;
3165
- font: inherit;
3166
- font-weight: 600;
3167
- cursor: pointer;
3168
- background: var(--_accent);
3169
- color: var(--_on-accent);
3170
- }
3171
-
3172
- .question-btn:disabled {
3173
- cursor: default;
3174
- opacity: 0.6;
3175
- }
3176
-
3177
- /* Skills — chips row + the /-command palette, above the input. */
3178
- .skill-chips {
3179
- display: flex;
3180
- flex-wrap: wrap;
3181
- gap: 6px;
3182
- padding: 10px 12px;
3183
- }
3184
-
3185
- /* The row is hidden whenever the host offers no skills, which is most elements
3186
- most of the time, and an author display beats the user-agent rule for the
3187
- hidden attribute. Without this it kept its padding: 20px of panel between
3188
- the transcript and the composer, under every placement, reading as the gap
3189
- under whatever the transcript ends with. */
3190
- .skill-chips[hidden] {
3191
- display: none;
3192
- }
3193
-
3194
- .skill-chip {
3195
- border: 1px solid var(--_border);
3196
- border-radius: 999px;
3197
- padding: 4px 12px;
3198
- font: inherit;
3199
- font-size: 0.9em;
3200
- cursor: pointer;
3201
- background: var(--_assistant-bg);
3202
- color: var(--_fg);
3203
- }
3204
-
3205
- .skill-chip:hover {
3206
- border-color: var(--_accent);
3207
- }
3208
-
3209
- /* Follow-up suggestion chips. Deliberately the skill chips' shape rather than a
3210
- second chip vocabulary -- both are "a question you could ask", and the only
3211
- difference is who chose it. Inside the transcript, so they scroll with the
3212
- answer they follow instead of hovering above the composer. */
3213
- .suggestions {
3214
- display: flex;
3215
- flex-wrap: wrap;
3216
- gap: 6px;
3217
- align-self: stretch;
3218
- }
3219
-
3220
- .suggestion-chip {
3221
- border: 1px solid var(--_border);
3222
- border-radius: 999px;
3223
- padding: 4px 12px;
3224
- font: inherit;
3225
- font-size: 0.9em;
3226
- text-align: left;
3227
- cursor: pointer;
3228
- background: var(--_assistant-bg);
3229
- color: var(--_fg);
3230
- }
3231
-
3232
- .suggestion-chip:hover,
3233
- .suggestion-chip:focus-visible {
3234
- border-color: var(--_accent);
3235
- }
3236
-
3237
- .skill-palette {
3238
- margin: 8px 12px 0;
3239
- display: flex;
3240
- flex-direction: column;
3241
- max-height: 220px;
3242
- overflow: auto;
3243
- /* Scrolling past the end of this must not scroll the page behind it. */
3244
- overscroll-behavior: contain;
3245
- background: var(--_bg);
3246
- border: 1px solid var(--_border);
3247
- border-radius: 8px;
3248
- box-shadow: 0 8px 24px rgba(20, 20, 50, 0.16);
3249
- }
3250
-
3251
- /* Closed nearly all of the time and mounted on every element. The display
3252
- above beats the user-agent rule for the hidden attribute, so without this a
3253
- closed palette still painted its margin, border and shadow as a line over
3254
- the composer. */
3255
- .skill-palette[hidden] {
3256
- display: none;
3257
- }
3258
-
3259
- .skill-item {
3260
- display: flex;
3261
- flex-direction: column;
3262
- gap: 2px;
3263
- align-items: flex-start;
3264
- padding: 8px 12px;
3265
- border: none;
3266
- background: none;
3267
- font: inherit;
3268
- text-align: left;
3269
- cursor: pointer;
3270
- color: var(--_fg);
3271
- }
3272
-
3273
- .skill-item[aria-selected="true"] {
3274
- background: var(--_assistant-bg);
3275
- }
3276
-
3277
- .skill-item-title {
3278
- font-weight: 600;
3279
- }
3280
-
3281
- .skill-item-desc {
3282
- font-size: 0.85em;
3283
- color: var(--_muted);
3284
- }
3285
-
3286
- /* The hint sits directly above the composer's top border, so a zero bottom
3287
- margin left the text touching the divider. */
3288
- .skill-hint {
3289
- margin: 8px 12px;
3290
- font-size: 0.85em;
3291
- line-height: 1.4;
3292
- color: var(--_danger);
3293
- }
3294
-
3295
- /* Chat-history drawer — a slide-over within the chat panel.
3296
- The hidden attribute stays the single source of truth for open/closed (no
3297
- JS-driven animation state): display is transitioned discretely, so on close
3298
- the drawer stays displayed for the whole exit and is removed only at the end,
3299
- which is what lets the backdrop fade and the panel slide out. */
3300
- .drawer {
3301
- position: absolute;
3302
- inset: 0;
3303
- z-index: 5;
3304
- display: flex;
3305
- /* Visible at once on the way in. Opening moves focus to the list in the
3306
- same task that unhides it, and an element still hidden cannot take focus:
3307
- a transition on visibility starts at its first value, so easing it in
3308
- left the drawer hidden at exactly that instant and the focus call was
3309
- dropped at every placement. Reduced motion shortens the transition
3310
- without removing it, so it had the same first frame. */
3311
- transition: visibility 0s;
3312
- }
3313
-
3314
- /* Closed. The overlay keeps its box (display, not none) so the backdrop and
3315
- panel inside it stay rendered and can transition both ways; visibility is
3316
- what takes the whole subtree out of the tab order, the a11y tree and hit
3317
- testing at rest. On the way out it is delayed rather than eased, so it
3318
- flips only once the slide has finished. A transition is read from the
3319
- state being entered, which is what lets the two directions differ. */
3320
- .drawer[hidden] {
3321
- display: flex;
3322
- visibility: hidden;
3323
- pointer-events: none;
3324
- transition: visibility 0s var(--_motion);
3325
- }
3326
-
3327
- .drawer[hidden] .drawer-backdrop {
3328
- opacity: 0;
3329
- }
3330
-
3331
- .drawer[hidden] .drawer-panel {
3332
- transform: translateX(-100%);
3333
- }
3334
-
3335
- .checkpoints {
3336
- position: absolute;
3337
- inset-block-start: 3rem;
3338
- inset-inline: 0.75rem;
3339
- z-index: 6;
3340
- display: flex;
3341
- flex-direction: column;
3342
- gap: 0.25rem;
3343
- padding: 0.5rem;
3344
- border: 1px solid var(--_border);
3345
- border-radius: 0.5rem;
3346
- background: var(--_assistant-bg);
3347
- box-shadow: 0 6px 24px rgb(0 0 0 / 12%);
3348
- max-height: 60%;
3349
- overflow-y: auto;
3350
- /* Scrolling past the end of this must not scroll the page behind it. */
3351
- overscroll-behavior: contain;
3352
- transform-origin: top center;
3353
- /* Visibility flips at once on the way in, for the drawer's reason: opening
3354
- focuses the panel in the same task that unhides it, and an element still
3355
- hidden cannot take focus. The fade and the scale still ease in. */
3356
- transition:
3357
- opacity var(--_motion) var(--_ease),
3358
- transform var(--_motion) var(--_ease),
3359
- visibility 0s;
3360
- }
3361
-
3362
- /* Same idiom as the drawer: laid out at rest, hidden by visibility, so the
3363
- popover can animate open and closed. The whole list is restated because a
3364
- transition is read from the state being entered; only visibility differs,
3365
- delayed rather than eased so it flips once the exit has played. */
3366
- .checkpoints[hidden] {
3367
- display: flex;
3368
- visibility: hidden;
3369
- pointer-events: none;
3370
- opacity: 0;
3371
- transform: scale(0.96) translateY(-6px);
3372
- transition:
3373
- opacity var(--_motion) var(--_ease),
3374
- transform var(--_motion) var(--_ease),
3375
- visibility 0s var(--_motion);
3376
- }
3377
-
3378
- .checkpoints-title {
3379
- font-size: 0.75rem;
3380
- font-weight: 600;
3381
- opacity: 0.7;
3382
- }
3383
-
3384
- .checkpoints-empty {
3385
- padding: 0.5rem 0.25rem;
3386
- font-size: 0.8125rem;
3387
- opacity: 0.7;
3388
- }
3389
-
3390
- /* A row is a label and two buttons, and nothing about the row itself is
3391
- pressable. It used to light up on hover, which is the affordance of something
3392
- clickable and made the buttons look like decoration on a clickable strip. The
3393
- resting surface groups the row instead, so hover can mean what it says: only
3394
- the buttons respond to it.
3395
-
3396
- It wraps for the same reason the tool-call head does. Every child but the label
3397
- is fixed-width, so in a narrow panel the label is the only thing that can give
3398
- -- and a flex-basis of zero lets it give everything. Adding the run id was
3399
- enough to crush "just now" to zero pixels: present, correct, and invisible.
3400
- Wrapping puts the buttons on their own line instead. */
3401
- .checkpoint-row {
3402
- display: flex;
3403
- flex-wrap: wrap;
3404
- align-items: center;
3405
- gap: 0.5rem;
3406
- padding: 0.3125rem 0.4375rem;
3407
- border-radius: 0.375rem;
3408
- background: var(--_hover);
3409
- }
3410
-
3411
- /* Grows into spare room, and refuses to shrink past the shortest thing it ever
3412
- says. A time is short and bounded, so there is no case for eliding it. */
3413
- .checkpoint-label {
3414
- flex: 1 1 auto;
3415
- min-width: 7ch;
3416
- font-size: 0.8125rem;
3417
- white-space: nowrap;
3418
- overflow: hidden;
3419
- text-overflow: ellipsis;
3420
- }
3421
-
3422
- /* When the label holds the run's first message, the time moves here: still worth
3423
- showing, no longer what identifies the row. Muted and unshrinkable, so it does
3424
- not compete with the words beside it. */
3425
- .checkpoint-time {
3426
- flex: 0 0 auto;
3427
- font-size: 0.6875rem;
3428
- opacity: 0.7;
3429
- white-space: nowrap;
3430
- }
3431
-
3432
- /* Enough of the run id to tell two runs apart when both say "just now". Muted
3433
- and monospaced: it is a reference, not a name. */
3434
- .checkpoint-id {
3435
- flex: 0 0 auto;
3436
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
3437
- font-size: 0.6875rem;
3438
- opacity: 0.55;
3439
- }
3440
-
3441
- /* On the panel's own surface, not the row's: the row now paints the hover token
3442
- itself, and a badge the same colour as what it sits on is not a badge. */
3443
- .checkpoint-branch {
3444
- font-size: 0.6875rem;
3445
- padding: 0 0.375rem;
3446
- border-radius: 999px;
3447
- background: var(--_assistant-bg);
3448
- opacity: 0.8;
3449
- }
3450
-
3451
- /* The two things in the row that actually do something, so they are the two
3452
- things that look like it: a filled surface at rest rather than a transparent
3453
- outline, which on top of the old row highlight was nearly invisible. */
3454
- .checkpoint-action {
3455
- font: inherit;
3456
- font-size: 0.75rem;
3457
- line-height: 1.4;
3458
- cursor: pointer;
3459
- padding: 0.1875rem 0.5625rem;
3460
- border: 1px solid var(--_border);
3461
- border-radius: 0.375rem;
3462
- background: var(--_bg);
3463
- color: inherit;
3464
- transition:
3465
- background var(--_motion) var(--_ease),
3466
- border-color var(--_motion) var(--_ease),
3467
- transform var(--_motion) var(--_ease);
3468
- }
3469
-
3470
- /* Resume is what a reader wants nine times in ten; fork is the deliberate choice
3471
- beside it. Filled and outlined, the same pair the confirmation and approval
3472
- cards already use for their primary and secondary action. */
3473
- .checkpoint-resume {
3474
- font-weight: 600;
3475
- border-color: var(--_accent);
3476
- background: var(--_accent);
3477
- color: var(--_on-accent);
3478
- }
3479
-
3480
- .checkpoint-fork:hover {
3481
- background: var(--_hover);
3482
- border-color: var(--_accent);
3483
- }
3484
-
3485
- /* The filled one cannot go lighter on hover without losing its contrast with the
3486
- white label, so it dims instead. */
3487
- .checkpoint-resume:hover {
3488
- opacity: 0.88;
3489
- }
3490
-
3491
- /* Pressed: a pixel down, so the click is felt as well as seen. */
3492
- .checkpoint-action:active {
3493
- transform: translateY(1px);
3494
- }
3495
-
3496
- /* Keyboard focus was invisible here, in a panel that traps focus and is reached
3497
- by Tab -- so the one navigation path guaranteed to land on these buttons was
3498
- the one with nothing to show for it. */
3499
- .checkpoint-action:focus-visible {
3500
- outline: 2px solid var(--_accent);
3501
- outline-offset: 2px;
3502
- }
3503
-
3504
- .drawer-backdrop {
3505
- position: absolute;
3506
- inset: 0;
3507
- background: rgba(20, 20, 50, 0.32);
3508
- opacity: 1;
3509
- transition: opacity var(--_motion) var(--_ease);
3510
- }
3511
-
3512
- .drawer-panel {
3513
- position: relative;
3514
- display: flex;
3515
- flex-direction: column;
3516
- width: min(300px, 85%);
3517
- height: 100%;
3518
- background: var(--_bg);
3519
- border-right: 1px solid var(--_border);
3520
- box-shadow: var(--_shadow);
3521
- overflow: hidden;
3522
- transform: none;
3523
- transition: transform var(--_motion) var(--_ease);
3524
- }
3525
-
3526
- .drawer-header {
3527
- display: flex;
3528
- align-items: center;
3529
- justify-content: space-between;
3530
- gap: var(--_space);
3531
- padding: var(--_pad);
3532
- border-bottom: 1px solid var(--_border);
3533
- }
3534
-
3535
- .drawer-title {
3536
- /* Takes the slack so the two controls group at the trailing edge. With
3537
- space-between and three items the middle one floats, which reads as the
3538
- title and the close button being a pair with New chat wedged between
3539
- them. Truncates rather than pushing them off the row. */
3540
- flex: 1 1 auto;
3541
- min-width: 0;
3542
- overflow: hidden;
3543
- text-overflow: ellipsis;
3544
- white-space: nowrap;
3545
- font-weight: 600;
3546
- }
3547
-
3548
- .drawer-new {
3549
- flex: 0 0 auto;
3550
- border: 1px solid var(--_border);
3551
- border-radius: var(--_radius);
3552
- background: var(--_bg);
3553
- color: var(--_accent);
3554
- padding: 4px 10px;
3555
- font: inherit;
3556
- font-size: 0.85em;
3557
- cursor: pointer;
3558
- }
3559
-
3560
- /* Narrows the list. Sits under the header rather than in it: the header's two
3561
- controls act on the conversation, and a field that filters what is below it
3562
- belongs with what it filters. */
3563
- /* What is waiting for the run to finish. Above the composer, beside the
3564
- attachment tray, because both are things already handed over and not yet
3565
- sent. Each chip is a button: pressing it takes that message back. */
3566
- .queued {
3567
- display: flex;
3568
- flex-wrap: wrap;
3569
- gap: var(--_space);
3570
- padding: 0 var(--_pad) var(--_space);
3571
- }
3572
-
3573
- /* Empty whenever no run is in flight, and hidden then. Same reason as the
3574
- palette: the display above would otherwise keep the bottom padding as dead
3575
- space over the composer. */
3576
- .queued[hidden] {
3577
- display: none;
3578
- }
3579
-
3580
- .queued-chip {
3581
- max-width: 100%;
3582
- overflow: hidden;
3583
- padding: 4px 10px;
3584
- border: 1px dashed var(--_border);
3585
- border-radius: var(--_msg-radius);
3586
- background: var(--_bg);
3587
- color: var(--_muted);
3588
- font: inherit;
3589
- font-size: 0.9em;
3590
- text-overflow: ellipsis;
3591
- white-space: nowrap;
3592
- cursor: pointer;
3593
- }
3594
-
3595
- .queued-chip:hover {
3596
- border-style: solid;
3597
- color: var(--_fg);
3598
- }
3599
-
3600
- .drawer-filter {
3601
- flex: 0 0 auto;
3602
- box-sizing: border-box;
3603
- width: calc(100% - var(--_pad) * 2);
3604
- margin: var(--_space) var(--_pad) 0;
3605
- padding: 6px 10px;
3606
- border: 1px solid var(--_border);
3607
- border-radius: var(--_radius);
3608
- background: var(--_input-bg);
3609
- color: var(--_fg);
3610
- font: inherit;
3611
- font-size: 0.9em;
3612
- }
3613
-
3614
- .drawer-filter::placeholder {
3615
- color: var(--_muted);
3616
- }
3617
-
3618
- .drawer-list {
3619
- flex: 1;
3620
- min-height: 0;
3621
- overflow-y: auto;
3622
- /* Scrolling past the end of this must not scroll the page behind it. */
3623
- overscroll-behavior: contain;
3624
- }
3625
-
3626
- .drawer-empty {
3627
- padding: var(--_pad);
3628
- font-size: 0.9em;
3629
- color: var(--_muted);
3630
- }
3631
-
3632
- .drawer-row {
3633
- display: flex;
3634
- align-items: stretch;
3635
- border-bottom: 1px solid var(--_border);
3636
- }
3637
-
3638
- .drawer-row--active {
3639
- background: var(--_assistant-bg);
3640
- }
3641
-
3642
- .drawer-row-select {
3643
- flex: 1;
3644
- min-width: 0;
3645
- display: flex;
3646
- flex-direction: column;
3647
- gap: 2px;
3648
- padding: 8px 12px;
3649
- border: none;
3650
- background: none;
3651
- color: inherit;
3652
- font: inherit;
3653
- text-align: left;
3654
- cursor: pointer;
3655
- }
3656
-
3657
- .drawer-row-title {
3658
- font-weight: 600;
3659
- overflow: hidden;
3660
- white-space: nowrap;
3661
- text-overflow: ellipsis;
3662
- }
3663
-
3664
- .drawer-row-time {
3665
- font-size: 0.72em;
3666
- color: var(--_muted);
3667
- }
3668
-
3669
- .drawer-row-preview {
3670
- font-size: 0.8em;
3671
- color: var(--_muted);
3672
- overflow: hidden;
3673
- white-space: nowrap;
3674
- text-overflow: ellipsis;
3675
- }
3676
-
3677
- .drawer-row-actions {
3678
- display: flex;
3679
- align-items: center;
3680
- gap: 2px;
3681
- padding: 0 6px;
3682
- }
3683
-
3684
- .drawer-row-rename,
3685
- .drawer-row-delete {
3686
- border: none;
3687
- background: none;
3688
- color: var(--_muted);
3689
- font-size: 0.9em;
3690
- padding: 4px;
3691
- cursor: pointer;
3692
- }
3693
-
3694
- .drawer-rename-input {
3695
- flex: 1;
3696
- min-width: 0;
3697
- margin: 6px 10px;
3698
- padding: 4px 8px;
3699
- border: 1px solid var(--_accent);
3700
- border-radius: 6px;
3701
- background: var(--_input-bg);
3702
- color: var(--_fg);
3703
- font: inherit;
3704
- }
3705
-
3706
- .drawer-confirm {
3707
- display: flex;
3708
- align-items: center;
3709
- gap: 8px;
3710
- padding: 8px 12px;
3711
- font-size: 0.85em;
3712
- }
3713
-
3714
- .drawer-confirm-label {
3715
- color: var(--_danger);
3716
- }
3717
-
3718
- .drawer-confirm-yes {
3719
- border: none;
3720
- border-radius: 6px;
3721
- background: var(--_danger);
3722
- color: var(--_on-danger);
3723
- padding: 3px 10px;
3724
- font: inherit;
3725
- cursor: pointer;
3726
- }
3727
-
3728
- .drawer-confirm-no {
3729
- border: 1px solid var(--_border);
3730
- border-radius: 6px;
3731
- background: none;
3732
- color: inherit;
3733
- padding: 3px 10px;
3734
- font: inherit;
3735
- cursor: pointer;
3736
- }
3737
-
3738
- /* Embedded placement: an inline, flush side panel rather than a dimmed,
3739
- floating slide-over. */
3740
- :host([placement="embedded"]) .drawer-backdrop {
3741
- background: none;
3742
- }
3743
-
3744
- /* The way back out of the list. It sits beside New chat, which is the control
3745
- it must not be mistaken for: one returns you to the conversation you were
3746
- reading, the other replaces it. */
3747
- .drawer-close {
3748
- flex: 0 0 auto;
3749
- display: inline-flex;
3750
- align-items: center;
3751
- justify-content: center;
3752
- width: 26px;
3753
- height: 26px;
3754
- padding: 0;
3755
- border: 0;
3756
- border-radius: 6px;
3757
- background: none;
3758
- color: inherit;
3759
- font: inherit;
3760
- font-size: 18px;
3761
- line-height: 1;
3762
- cursor: pointer;
3763
- }
3764
-
3765
- .drawer-close:hover {
3766
- background: var(--_hover);
3767
- }
3768
-
3769
- :host([placement="embedded"]) .drawer-panel {
3770
- width: 100%;
3771
- border-right: none;
3772
- box-shadow: none;
3773
- }
3774
- `;
1
+ // The shadow stylesheet, composed from one module per concern. Each module is
2
+ // a plain string constant so the element can adopt the whole sheet without a
3
+ // build-time CSS pipeline, and the shadow boundary scopes every class name.
4
+ //
5
+ // The list below is in cascade order, and that order is load-bearing: two
6
+ // rules of equal specificity resolve by which comes later, and several
7
+ // modules restate a state another module set at rest. A module moves in this
8
+ // list only with a browser test that pins what the move changes. The split
9
+ // that produced these modules moved no rule, so the composed sheet is the
10
+ // single stylesheet it replaced: the rules and their order are unchanged and
11
+ // only comments differ, which is also why a few small rules still sit in a
12
+ // neighbouring concern's module.
13
+ //
14
+ // Every module is a template literal: a backtick or a dollar-brace inside a
15
+ // CSS comment ends it, and tsc reports the error far from the edit.
16
+
17
+ import { CHART_STYLES } from "./charts/chart_styles.js";
18
+ import { ATTACHMENT_STYLES } from "./composer/attachment_styles.js";
19
+ import { COMPOSER_STYLES } from "./composer/composer_styles.js";
20
+ import { SKILL_STYLES } from "./composer/skill_styles.js";
21
+ import { HISTORY_STYLES } from "./history/history_styles.js";
22
+ import { APPROVAL_STYLES } from "./interrupts/approval_styles.js";
23
+ import { CONFIRMATION_STYLES } from "./interrupts/confirmation_styles.js";
24
+ import { QUESTION_STYLES } from "./interrupts/question_styles.js";
25
+ import { COLLAPSE_STYLES } from "./placement/collapse_styles.js";
26
+ import { EMBEDDED_STYLES } from "./placement/embedded_styles.js";
27
+ import { LAUNCHER_STYLES } from "./placement/launcher_styles.js";
28
+ import { PAGE_STYLES } from "./placement/page_styles.js";
29
+ import { PRESET_STYLES } from "./placement/preset_styles.js";
30
+ import { RESIZE_STYLES } from "./placement/resize_styles.js";
31
+ import { SIDEBAR_STYLES } from "./placement/sidebar_styles.js";
32
+ import { SMALL_VIEWPORT_STYLES } from "./placement/small_viewport_styles.js";
33
+ import { RUN_NOTICE_STYLES } from "./progress/run_notice_styles.js";
34
+ import { THOUGHTS_STYLES } from "./progress/thoughts_styles.js";
35
+ import { TOOL_GROUP_STYLES } from "./progress/tool_group_styles.js";
36
+ import { TOOL_STEP_STYLES } from "./progress/tool_step_styles.js";
37
+ import { FRAME_STYLES } from "./shell/frame_styles.js";
38
+ import { THEME_STYLES } from "./shell/theme_styles.js";
39
+ import { TOKEN_STYLES } from "./shell/token_styles.js";
40
+ import { CLASSIC_STYLES } from "./themes/classic_styles.js";
41
+ import { CLASSIC_TOKENS } from "./themes/classic_tokens.js";
42
+ import { CODE_TOKENS } from "./themes/code_tokens.js";
43
+ import { LEDGER_STYLES } from "./themes/ledger_styles.js";
44
+ import { LEDGER_TOKENS } from "./themes/ledger_tokens.js";
45
+ import { MODERN_STYLES } from "./themes/modern_styles.js";
46
+ import { QUIET_STYLES } from "./themes/quiet_styles.js";
47
+ import { SCHEME_STYLES } from "./themes/scheme_styles.js";
48
+ import { SOFT_STYLES } from "./themes/soft_styles.js";
49
+ import { SOFT_TOKENS } from "./themes/soft_tokens.js";
50
+ import { ATTACHMENT_CHIP_STYLES } from "./transcript/attachment_chip_styles.js";
51
+ import { GREETING_STYLES } from "./transcript/greeting_styles.js";
52
+ import { MESSAGE_ACTION_STYLES } from "./transcript/message_action_styles.js";
53
+ import { MESSAGE_STYLES } from "./transcript/message_styles.js";
54
+ import { TRANSCRIPT_STYLES } from "./transcript/transcript_styles.js";
55
+
56
+ export const STYLES: string = [
57
+ // Quiet's tokens are the defaults; each theme's tokens follow, then the
58
+ // scheme, then density, so density="compact" outranks a theme's own scale.
59
+ TOKEN_STYLES,
60
+ CLASSIC_TOKENS,
61
+ CODE_TOKENS,
62
+ SOFT_TOKENS,
63
+ LEDGER_TOKENS,
64
+ SCHEME_STYLES,
65
+ THEME_STYLES,
66
+ PRESET_STYLES,
67
+ PAGE_STYLES,
68
+ SMALL_VIEWPORT_STYLES,
69
+ SIDEBAR_STYLES,
70
+ LAUNCHER_STYLES,
71
+ EMBEDDED_STYLES,
72
+ FRAME_STYLES,
73
+ COLLAPSE_STYLES,
74
+ TRANSCRIPT_STYLES,
75
+ GREETING_STYLES,
76
+ MESSAGE_STYLES,
77
+ THOUGHTS_STYLES,
78
+ TOOL_STEP_STYLES,
79
+ RESIZE_STYLES,
80
+ COMPOSER_STYLES,
81
+ ATTACHMENT_STYLES,
82
+ CHART_STYLES,
83
+ RUN_NOTICE_STYLES,
84
+ ATTACHMENT_CHIP_STYLES,
85
+ CONFIRMATION_STYLES,
86
+ MESSAGE_ACTION_STYLES,
87
+ APPROVAL_STYLES,
88
+ QUESTION_STYLES,
89
+ SKILL_STYLES,
90
+ HISTORY_STYLES,
91
+ // The step group restates, inside a group, card rules the modules above set
92
+ // at rest; it outweighs them rather than relying on order, and sits ahead
93
+ // of the themes so a theme restating a group rule at equal weight wins.
94
+ TOOL_GROUP_STYLES,
95
+ // Each theme's structure comes last: it restates rules the modules above
96
+ // set at rest, at equal weight. Classic and code have no structural
97
+ // module; they are the base stylesheet, and classic adds only its
98
+ // contrast fixes.
99
+ CLASSIC_STYLES,
100
+ MODERN_STYLES,
101
+ QUIET_STYLES,
102
+ SOFT_STYLES,
103
+ LEDGER_STYLES,
104
+ ].join("");