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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +192 -1
  2. package/README.md +404 -28
  3. package/dist/ag-ui-web-component.bundle.js +781 -451
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +29 -0
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +93 -1
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/core/create_http_agent.d.ts +9 -0
  10. package/dist/core/create_http_agent.d.ts.map +1 -1
  11. package/dist/core/remote_conversation_store.d.ts +8 -1
  12. package/dist/core/remote_conversation_store.d.ts.map +1 -1
  13. package/dist/core/run_index.d.ts +8 -1
  14. package/dist/core/run_index.d.ts.map +1 -1
  15. package/dist/core/transcribe_audio.d.ts +7 -0
  16. package/dist/core/transcribe_audio.d.ts.map +1 -1
  17. package/dist/core/upload_attachment.d.ts +9 -0
  18. package/dist/core/upload_attachment.d.ts.map +1 -1
  19. package/dist/core/utils.d.ts +12 -0
  20. package/dist/core/utils.d.ts.map +1 -0
  21. package/dist/dom/animations.d.ts +51 -11
  22. package/dist/dom/animations.d.ts.map +1 -1
  23. package/dist/dom/dom_driver.d.ts +12 -7
  24. package/dist/dom/dom_driver.d.ts.map +1 -1
  25. package/dist/index.d.ts +3 -3
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1275 -520
  28. package/dist/index.js.map +3 -3
  29. package/dist/ui/styles.d.ts +1 -1
  30. package/dist/ui/styles.d.ts.map +1 -1
  31. package/dist/ui/ui_strings.d.ts +8 -1
  32. package/dist/ui/ui_strings.d.ts.map +1 -1
  33. package/dist/ui/voice_input.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/constants.ts +35 -0
  36. package/src/core/ag_ui_chat.ts +452 -55
  37. package/src/core/create_http_agent.ts +14 -3
  38. package/src/core/remote_conversation_store.ts +25 -6
  39. package/src/core/run_index.ts +27 -5
  40. package/src/core/transcribe_audio.ts +20 -5
  41. package/src/core/upload_attachment.ts +13 -0
  42. package/src/core/utils.ts +18 -0
  43. package/src/dom/animations.ts +175 -31
  44. package/src/dom/dom_driver.ts +18 -12
  45. package/src/index.ts +4 -0
  46. package/src/ui/styles.ts +751 -421
  47. package/src/ui/ui_strings.ts +9 -1
  48. package/src/ui/voice_input.ts +7 -1
  49. package/src/version.ts +1 -1
package/src/ui/styles.ts CHANGED
@@ -3,151 +3,216 @@
3
3
  // CSS pipeline. Scoped by the shadow boundary, so class names stay terse.
4
4
 
5
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 a page that put the tokens
13
+ on a wrapper would see no effect at all, which is the one thing the API is
14
+ documented to support. Reading the public name with the default as a var()
15
+ fallback leaves it undeclared on the element, so an ancestor's value is
16
+ inherited normally while a value aimed at the element still wins over it.
17
+
18
+ Two invariants hold this together:
19
+ 1. No rule outside this file's :host blocks may reference a public name
20
+ directly. A public name read outside these blocks resolves to nothing and
21
+ silently drops its declaration rather than erroring, so the miss shows up
22
+ as a missing colour, not a failure.
23
+ 2. Every --_* alias in use is declared here. Aliases inherit like any custom
24
+ property, so an undeclared one would pick up a same-named property from
25
+ the host page; declaring it on :host shields the shadow tree from that. */
6
26
  :host {
7
27
  /* Colors */
8
- --ag-ui-bg: #ffffff;
9
- --ag-ui-fg: #1a1a2e;
10
- --ag-ui-accent: #4f46e5;
11
- --ag-ui-user-bg: #4f46e5;
12
- --ag-ui-user-fg: #ffffff;
13
- --ag-ui-assistant-bg: #f1f1f6;
14
- --ag-ui-hover: #e7e7ee;
15
- --ag-ui-input-bg: var(--ag-ui-bg);
16
- --ag-ui-tool-bg: var(--ag-ui-assistant-bg);
17
- --ag-ui-tool-fg: var(--ag-ui-accent);
18
- --ag-ui-header-bg: var(--ag-ui-accent);
19
- --ag-ui-header-fg: #ffffff;
20
- --ag-ui-border: #e2e2ec;
21
- --ag-ui-radius: 12px;
28
+ --_bg: var(--ag-ui-bg, #ffffff);
29
+ --_fg: var(--ag-ui-fg, #1a1a2e);
30
+ --_accent: var(--ag-ui-accent, #4f46e5);
31
+ --_user-bg: var(--ag-ui-user-bg, #4f46e5);
32
+ --_user-fg: var(--ag-ui-user-fg, #ffffff);
33
+ --_assistant-bg: var(--ag-ui-assistant-bg, #f1f1f6);
34
+ --_hover: var(--ag-ui-hover, #e7e7ee);
35
+ --_input-bg: var(--ag-ui-input-bg, var(--_bg));
36
+ --_tool-bg: var(--ag-ui-tool-bg, var(--_assistant-bg));
37
+ --_tool-fg: var(--ag-ui-tool-fg, var(--_accent));
38
+ --_header-bg: var(--ag-ui-header-bg, var(--_accent));
39
+ --_header-fg: var(--ag-ui-header-fg, #ffffff);
40
+ --_border: var(--ag-ui-border, #e2e2ec);
41
+ --_radius: var(--ag-ui-radius, 12px);
42
+
43
+ /* Body text and raised chrome. Both are referenced by the code-block copy
44
+ button; neither had a default before, so the declarations reading them
45
+ were dropped and the button fell back to the inherited colour over a
46
+ transparent box. The defaults below restate exactly that, so this is a
47
+ rename with no repaint — see the note on .code-copy. */
48
+ --_text: var(--ag-ui-text, var(--_fg));
49
+ --_surface: var(--ag-ui-surface, transparent);
22
50
 
23
51
  /* Status accents for tool-call cards. */
24
- --ag-ui-success: #15803d;
25
- --ag-ui-danger: #b91c1c;
26
- --ag-ui-muted: #6b7280;
52
+ --_success: var(--ag-ui-success, #15803d);
53
+ --_danger: var(--ag-ui-danger, #b91c1c);
54
+ --_muted: var(--ag-ui-muted, #6b7280);
27
55
 
28
56
  /* Tool-call status icon glyphs (override to re-theme) + spinner speed.
29
57
  The pending state is the animated ring; the settled states use these. */
30
- --ag-ui-tool-icon-done: "✓";
31
- --ag-ui-tool-icon-error: "✕";
32
- --ag-ui-tool-icon-declined: "⊘";
33
- --ag-ui-tool-spin-duration: 0.7s;
58
+ --_tool-icon-done: var(--ag-ui-tool-icon-done, "✓");
59
+ --_tool-icon-error: var(--ag-ui-tool-icon-error, "✕");
60
+ --_tool-icon-declined: var(--ag-ui-tool-icon-declined, "⊘");
61
+ --_tool-spin-duration: var(--ag-ui-tool-spin-duration, 0.7s);
34
62
 
35
63
  /* Answer well (opt-in via data-answer-well) — boxes a whole assistant turn. */
36
- --ag-ui-well-bg: transparent;
37
- --ag-ui-well-border: var(--ag-ui-border);
64
+ --_well-bg: var(--ag-ui-well-bg, transparent);
65
+ --_well-border: var(--ag-ui-well-border, var(--_border));
38
66
 
39
67
  /* Surface — set --ag-ui-shadow: none for a flush, embedded panel. */
40
- --ag-ui-shadow: 0 12px 32px rgba(20, 20, 50, 0.18);
41
- --ag-ui-font: inherit;
42
- --ag-ui-font-size: 14px;
43
- --ag-ui-code-font: ui-monospace, "SF Mono", Menlo, monospace;
68
+ --_shadow: var(--ag-ui-shadow, 0 12px 32px rgba(20, 20, 50, 0.18));
69
+ --_font: var(--ag-ui-font, inherit);
70
+ --_font-size: var(--ag-ui-font-size, 14px);
71
+ --_code-font: var(--ag-ui-code-font, ui-monospace, "SF Mono", Menlo, monospace);
72
+
73
+ /* Header / launcher icon box. */
74
+ --_icon-size: var(--ag-ui-icon-size, 22px);
75
+ --_icon-radius: var(--ag-ui-icon-radius, 4px);
76
+
77
+ /* Composer — one bordered surface holding the field and its tool row. */
78
+ --_composer-radius: var(--ag-ui-composer-radius, 14px);
79
+ --_composer-max-height: var(--ag-ui-composer-max-height, 40vh);
80
+ --_tool-btn-size: var(--ag-ui-tool-btn-size, 30px);
81
+ --_send-size: var(--ag-ui-send-size, 30px);
82
+ --_glyph-size: var(--ag-ui-glyph-size, 18px);
83
+ --_glyph-stroke: var(--ag-ui-glyph-stroke, 1.75);
84
+
85
+ /* Unread badge on the launcher. */
86
+ --_badge-bg: var(--ag-ui-badge-bg, var(--_danger));
87
+ --_badge-fg: var(--ag-ui-badge-fg, #ffffff);
88
+ --_badge-size: var(--ag-ui-badge-size, 18px);
89
+ --_badge-font-size: var(--ag-ui-badge-font-size, 11px);
90
+
91
+ /* The floating launcher a collapsed widget shrinks to. */
92
+ --_launcher-size: var(--ag-ui-launcher-size, 56px);
93
+ --_launcher-bg: var(--ag-ui-launcher-bg, var(--_header-bg));
94
+ --_launcher-fg: var(--ag-ui-launcher-fg, var(--_header-fg));
95
+ --_launcher-radius: var(--ag-ui-launcher-radius, 50%);
96
+ --_launcher-icon-size: var(--ag-ui-launcher-icon-size, 26px);
97
+ --_launcher-inset: var(--ag-ui-launcher-inset, auto 0 0 auto);
98
+
99
+ /* Motion. One duration and two curves drive every collapse, expand and
100
+ slide-over, so the whole widget accelerates and settles as one thing.
101
+ The default curve is a long-tailed ease-out (a fast start that decelerates
102
+ into place); the pop curve overshoots slightly, for something arriving. */
103
+ --_motion: var(--ag-ui-motion, 0.28s);
104
+ --_ease: var(--ag-ui-ease, cubic-bezier(0.32, 0.72, 0, 1));
105
+ --_ease-pop: var(--ag-ui-ease-pop, cubic-bezier(0.34, 1.36, 0.64, 1));
44
106
 
45
107
  /* Spacing — the density preset overrides these. */
46
- --ag-ui-space: 10px;
47
- --ag-ui-pad: 16px;
48
- --ag-ui-msg-pad: 8px 12px;
49
- --ag-ui-msg-radius: 14px;
108
+ --_space: var(--ag-ui-space, 10px);
109
+ --_pad: var(--ag-ui-pad, 16px);
110
+ --_msg-pad: var(--ag-ui-msg-pad, 8px 12px);
111
+ --_msg-radius: var(--ag-ui-msg-radius, 14px);
50
112
 
51
113
  /* Layout — override from outside to dock the widget anywhere.
52
114
  Set --ag-ui-position: static (and place this element in your own
53
115
  grid/flex layout) to embed it in the page flow instead of floating. */
54
- --ag-ui-position: fixed;
55
- --ag-ui-z-index: 2147483000;
56
- --ag-ui-width: 380px;
57
- --ag-ui-height: 560px;
58
- --ag-ui-inset: auto 24px 24px auto;
59
- --ag-ui-max-width: calc(100vw - 48px);
60
- --ag-ui-max-height: calc(100vh - 48px);
116
+ --_position: var(--ag-ui-position, fixed);
117
+ --_z-index: var(--ag-ui-z-index, 2147483000);
118
+ --_width: var(--ag-ui-width, 380px);
119
+ --_height: var(--ag-ui-height, 560px);
120
+ --_inset: var(--ag-ui-inset, auto 24px 24px auto);
121
+ --_max-width: var(--ag-ui-max-width, calc(100vw - 48px));
122
+ --_max-height: var(--ag-ui-max-height, calc(100vh - 48px));
61
123
  /* Reading-column width for placement="page" (full-bleed, centred content). */
62
- --ag-ui-content-max-width: 820px;
63
-
64
- position: var(--ag-ui-position);
65
- inset: var(--ag-ui-inset);
66
- z-index: var(--ag-ui-z-index);
67
- width: var(--ag-ui-width);
68
- max-width: var(--ag-ui-max-width);
69
- height: var(--ag-ui-height);
70
- max-height: var(--ag-ui-max-height);
124
+ --_content-max-width: var(--ag-ui-content-max-width, 820px);
125
+ /* Slim rail the sidebar placement collapses to. Only that placement reads
126
+ it, but it is declared here so invariant 2 holds for every alias. */
127
+ --_rail-width: var(--ag-ui-rail-width, 52px);
128
+
129
+ position: var(--_position);
130
+ inset: var(--_inset);
131
+ z-index: var(--_z-index);
132
+ width: var(--_width);
133
+ max-width: var(--_max-width);
134
+ height: var(--_height);
135
+ max-height: var(--_max-height);
71
136
  display: flex;
72
- font-family: var(--ag-ui-font);
73
- font-size: var(--ag-ui-font-size);
74
- color: var(--ag-ui-fg);
137
+ font-family: var(--_font);
138
+ font-size: var(--_font-size);
139
+ color: var(--_fg);
75
140
  }
76
141
 
77
142
  /* ── Themes ─────────────────────────────────────────────────────────────
78
143
  Themes only re-set the colour variables; layout/spacing are unaffected.
79
144
  theme="auto" follows the OS via prefers-color-scheme. */
80
145
  :host([theme="dark"]) {
81
- --ag-ui-bg: #15151f;
82
- --ag-ui-fg: #e8e8f2;
83
- --ag-ui-assistant-bg: #25253a;
84
- --ag-ui-hover: #303049;
85
- --ag-ui-header-bg: #1f1f30;
86
- --ag-ui-header-fg: #e8e8f2;
87
- --ag-ui-border: #33334a;
88
- --ag-ui-muted: #9aa0b4;
89
- --ag-ui-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);
146
+ --_bg: var(--ag-ui-bg, #15151f);
147
+ --_fg: var(--ag-ui-fg, #e8e8f2);
148
+ --_assistant-bg: var(--ag-ui-assistant-bg, #25253a);
149
+ --_hover: var(--ag-ui-hover, #303049);
150
+ --_header-bg: var(--ag-ui-header-bg, #1f1f30);
151
+ --_header-fg: var(--ag-ui-header-fg, #e8e8f2);
152
+ --_border: var(--ag-ui-border, #33334a);
153
+ --_muted: var(--ag-ui-muted, #9aa0b4);
154
+ --_shadow: var(--ag-ui-shadow, 0 12px 32px rgba(0, 0, 0, 0.5));
90
155
  }
91
156
 
92
157
  @media (prefers-color-scheme: dark) {
93
158
  :host([theme="auto"]) {
94
- --ag-ui-bg: #15151f;
95
- --ag-ui-fg: #e8e8f2;
96
- --ag-ui-assistant-bg: #25253a;
97
- --ag-ui-hover: #303049;
98
- --ag-ui-header-bg: #1f1f30;
99
- --ag-ui-header-fg: #e8e8f2;
100
- --ag-ui-border: #33334a;
101
- --ag-ui-muted: #9aa0b4;
102
- --ag-ui-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);
159
+ --_bg: var(--ag-ui-bg, #15151f);
160
+ --_fg: var(--ag-ui-fg, #e8e8f2);
161
+ --_assistant-bg: var(--ag-ui-assistant-bg, #25253a);
162
+ --_hover: var(--ag-ui-hover, #303049);
163
+ --_header-bg: var(--ag-ui-header-bg, #1f1f30);
164
+ --_header-fg: var(--ag-ui-header-fg, #e8e8f2);
165
+ --_border: var(--ag-ui-border, #33334a);
166
+ --_muted: var(--ag-ui-muted, #9aa0b4);
167
+ --_shadow: var(--ag-ui-shadow, 0 12px 32px rgba(0, 0, 0, 0.5));
103
168
  }
104
169
  }
105
170
 
106
171
  /* A terminal-flavoured "code" theme: dark, monospace, green accent. */
107
172
  :host([theme="code"]) {
108
- --ag-ui-bg: #0d1117;
109
- --ag-ui-fg: #c9d1d9;
110
- --ag-ui-accent: #3fb950;
111
- --ag-ui-user-bg: #238636;
112
- --ag-ui-assistant-bg: #161b22;
113
- --ag-ui-hover: #21262d;
114
- --ag-ui-header-bg: #010409;
115
- --ag-ui-header-fg: #c9d1d9;
116
- --ag-ui-border: #30363d;
117
- --ag-ui-muted: #8b949e;
118
- --ag-ui-font: var(--ag-ui-code-font);
119
- --ag-ui-shadow: 0 12px 32px rgba(0, 0, 0, 0.6);
173
+ --_bg: var(--ag-ui-bg, #0d1117);
174
+ --_fg: var(--ag-ui-fg, #c9d1d9);
175
+ --_accent: var(--ag-ui-accent, #3fb950);
176
+ --_user-bg: var(--ag-ui-user-bg, #238636);
177
+ --_assistant-bg: var(--ag-ui-assistant-bg, #161b22);
178
+ --_hover: var(--ag-ui-hover, #21262d);
179
+ --_header-bg: var(--ag-ui-header-bg, #010409);
180
+ --_header-fg: var(--ag-ui-header-fg, #c9d1d9);
181
+ --_border: var(--ag-ui-border, #30363d);
182
+ --_muted: var(--ag-ui-muted, #8b949e);
183
+ --_font: var(--ag-ui-font, var(--_code-font));
184
+ --_shadow: var(--ag-ui-shadow, 0 12px 32px rgba(0, 0, 0, 0.6));
120
185
  }
121
186
 
122
187
  /* ── Density ────────────────────────────────────────────────────────────── */
123
188
  :host([density="compact"]) {
124
- --ag-ui-font-size: 13px;
125
- --ag-ui-space: 6px;
126
- --ag-ui-pad: 10px;
127
- --ag-ui-msg-pad: 5px 9px;
128
- --ag-ui-msg-radius: 10px;
189
+ --_font-size: var(--ag-ui-font-size, 13px);
190
+ --_space: var(--ag-ui-space, 6px);
191
+ --_pad: var(--ag-ui-pad, 10px);
192
+ --_msg-pad: var(--ag-ui-msg-pad, 5px 9px);
193
+ --_msg-radius: var(--ag-ui-msg-radius, 10px);
129
194
  }
130
195
 
131
196
  /* ── Placement presets ──────────────────────────────────────────────────── */
132
197
  :host([placement="bottom-left"]) {
133
- --ag-ui-inset: auto auto 24px 24px;
198
+ --_inset: var(--ag-ui-inset, auto auto 24px 24px);
134
199
  }
135
200
 
136
201
  :host([placement="side"]) {
137
- --ag-ui-inset: 0 0 0 auto;
138
- --ag-ui-width: 420px;
139
- --ag-ui-height: 100vh;
140
- --ag-ui-max-height: 100vh;
141
- --ag-ui-radius: 0;
202
+ --_inset: var(--ag-ui-inset, 0 0 0 auto);
203
+ --_width: var(--ag-ui-width, 420px);
204
+ --_height: var(--ag-ui-height, 100vh);
205
+ --_max-height: var(--ag-ui-max-height, 100vh);
206
+ --_radius: var(--ag-ui-radius, 0);
142
207
  }
143
208
 
144
209
  :host([placement="full"]) {
145
- --ag-ui-inset: 0;
146
- --ag-ui-width: 100vw;
147
- --ag-ui-height: 100vh;
148
- --ag-ui-max-width: 100vw;
149
- --ag-ui-max-height: 100vh;
150
- --ag-ui-radius: 0;
210
+ --_inset: var(--ag-ui-inset, 0);
211
+ --_width: var(--ag-ui-width, 100vw);
212
+ --_height: var(--ag-ui-height, 100vh);
213
+ --_max-width: var(--ag-ui-max-width, 100vw);
214
+ --_max-height: var(--ag-ui-max-height, 100vh);
215
+ --_radius: var(--ag-ui-radius, 0);
151
216
  }
152
217
 
153
218
  /* Page: full-bleed background with a centred reading column. Unlike
@@ -156,85 +221,20 @@ export const STYLES = `
156
221
  padding on the scroll area + composer (no per-row wrapper), so user pills
157
222
  still right-align and the assistant well spans the column. */
158
223
  :host([placement="page"]) {
159
- --ag-ui-inset: 0;
160
- --ag-ui-width: 100vw;
161
- --ag-ui-height: 100vh;
162
- --ag-ui-max-width: 100vw;
163
- --ag-ui-max-height: 100vh;
164
- --ag-ui-radius: 0;
224
+ --_inset: var(--ag-ui-inset, 0);
225
+ --_width: var(--ag-ui-width, 100vw);
226
+ --_height: var(--ag-ui-height, 100vh);
227
+ --_max-width: var(--ag-ui-max-width, 100vw);
228
+ --_max-height: var(--ag-ui-max-height, 100vh);
229
+ --_radius: var(--ag-ui-radius, 0);
165
230
  }
166
231
 
167
232
  :host([placement="page"]) .messages {
168
- padding-inline: max(var(--ag-ui-pad), calc((100% - var(--ag-ui-content-max-width)) / 2));
233
+ padding-inline: max(var(--_pad), calc((100% - var(--_content-max-width)) / 2));
169
234
  }
170
235
 
171
- :host([placement="page"]) /* Resize handle: a corner grip on a floating panel, an edge grip on a docked
172
- one. Absent entirely where the placement is full-bleed, since there is
173
- nothing to drag. */
174
- .resize-handle {
175
- position: absolute;
176
- z-index: 2;
177
- background: transparent;
178
- touch-action: none;
179
- }
180
-
181
- .resize-handle:focus-visible {
182
- outline: 2px solid var(--ag-ui-accent);
183
- outline-offset: -2px;
184
- }
185
-
186
- /* The grip sits at the corner the panel grows toward. Which corner that is
187
- depends on the host's layout, not on placement, so the element measures it
188
- and stamps data-resize-anchor with the edges that stay put.
189
-
190
- Default (bottom-right pinned, the floating case) puts the grip top-left. */
191
- .resize-handle {
192
- top: 0;
193
- left: 0;
194
- width: 14px;
195
- height: 14px;
196
- cursor: nwse-resize;
197
- }
198
-
199
- :host([data-resize-anchor~="left"]) .resize-handle {
200
- left: auto;
201
- right: 0;
202
- }
203
-
204
- :host([data-resize-anchor~="top"]) .resize-handle {
205
- top: auto;
206
- bottom: 0;
207
- }
208
-
209
- /* One axis flipped means the drag runs along the other diagonal. */
210
- :host([data-resize-anchor="top-right"]) .resize-handle,
211
- :host([data-resize-anchor="bottom-left"]) .resize-handle {
212
- cursor: nesw-resize;
213
- }
214
-
215
- /* Docked: the placement owns the height, so only the inner edge is the user's. */
216
- :host([placement="sidebar"]) .resize-handle,
217
- :host([placement="side"]) .resize-handle {
218
- top: 0;
219
- bottom: auto;
220
- width: 8px;
221
- height: 100%;
222
- cursor: ew-resize;
223
- }
224
-
225
- /* Full-bleed: nothing to drag. */
226
- :host([placement="full"]) .resize-handle,
227
- :host([placement="page"]) .resize-handle {
228
- display: none;
229
- }
230
-
231
- .resize-handle[data-dragging] {
232
- background: var(--ag-ui-accent);
233
- opacity: 0.35;
234
- }
235
-
236
- .input-row {
237
- padding-inline: max(12px, calc((100% - var(--ag-ui-content-max-width)) / 2));
236
+ :host([placement="page"]) .input-row {
237
+ padding-inline: max(12px, calc((100% - var(--_content-max-width)) / 2));
238
238
  }
239
239
 
240
240
  /* The rows between the message list and the composer (skill chips, the
@@ -243,12 +243,12 @@ export const STYLES = `
243
243
  margin-based, so each gets its own inline axis nudged by the same gutter. */
244
244
  :host([placement="page"]) .skill-chips,
245
245
  :host([placement="page"]) .attachment-tray {
246
- padding-inline: max(12px, calc((100% - var(--ag-ui-content-max-width)) / 2));
246
+ padding-inline: max(12px, calc((100% - var(--_content-max-width)) / 2));
247
247
  }
248
248
 
249
249
  :host([placement="page"]) .skill-palette,
250
250
  :host([placement="page"]) .skill-hint {
251
- margin-inline: max(12px, calc((100% - var(--ag-ui-content-max-width)) / 2));
251
+ margin-inline: max(12px, calc((100% - var(--_content-max-width)) / 2));
252
252
  }
253
253
 
254
254
  /* In the reading column the assistant well uses the full width; the user
@@ -263,75 +263,179 @@ export const STYLES = `
263
263
  --ag-ui-position: static (and place this element in your own layout) for a
264
264
  host-managed push instead. */
265
265
  :host([placement="sidebar"]) {
266
- --ag-ui-inset: 0 0 0 auto;
267
- --ag-ui-width: 420px;
268
- --ag-ui-height: 100vh;
269
- --ag-ui-max-height: 100vh;
270
- --ag-ui-radius: 0;
271
- --ag-ui-rail-width: 52px;
272
- transition: width 0.28s ease;
266
+ --_inset: var(--ag-ui-inset, 0 0 0 auto);
267
+ --_width: var(--ag-ui-width, 420px);
268
+ --_height: var(--ag-ui-height, 100vh);
269
+ --_max-height: var(--ag-ui-max-height, 100vh);
270
+ --_radius: var(--ag-ui-radius, 0);
271
+ transition: width var(--_motion) var(--_ease);
273
272
  }
274
273
 
275
274
  :host([placement="sidebar"][data-side="left"]) {
276
- --ag-ui-inset: 0 auto 0 0;
275
+ --_inset: var(--ag-ui-inset, 0 auto 0 0);
277
276
  }
278
277
 
278
+ /* The docked panel is pinned to the edge it docks against rather than filling
279
+ the host as a flex child. Collapsing shrinks the host to the rail width, and
280
+ a flex child would be squashed to 52px on the way out instead of sliding out
281
+ at full width. */
279
282
  :host([placement="sidebar"]) .chat {
280
- transition: transform 0.28s ease;
283
+ position: absolute;
284
+ inset: 0 0 0 auto;
285
+ width: var(--_width);
286
+ transform-origin: right center;
287
+ }
288
+
289
+ :host([placement="sidebar"][data-side="left"]) .chat {
290
+ inset: 0 auto 0 0;
291
+ transform-origin: left center;
281
292
  }
282
293
 
283
- /* Collapsed sidebar: shrink the host to the rail width, hide the panel, and
284
- reveal the rail. Higher specificity than the generic collapse rules, so it
285
- wins regardless of source order. */
294
+ /* Collapsed sidebar: shrink the host to the rail width and slide the panel out
295
+ through the edge it docks against. Higher specificity than the generic
296
+ collapse rules, so it wins regardless of source order. */
286
297
  :host([placement="sidebar"][collapsed]) {
287
- width: var(--ag-ui-rail-width);
298
+ width: var(--_rail-width);
288
299
  height: 100vh;
289
300
  max-height: 100vh;
290
301
  bottom: 0;
302
+ pointer-events: auto;
291
303
  }
292
304
 
293
305
  :host([placement="sidebar"][collapsed]) .chat {
294
- display: none;
306
+ transform: translateX(100%);
295
307
  }
296
308
 
297
- /* The rail is a sibling of the panel (so it survives the panel being hidden);
298
- shown only for a collapsed sidebar. */
299
- .rail {
300
- display: none;
309
+ :host([placement="sidebar"][data-side="left"][collapsed]) .chat {
310
+ transform: translateX(-100%);
311
+ }
312
+
313
+ @media (prefers-reduced-motion: reduce) {
314
+ :host([placement="sidebar"]),
315
+ :host([placement="sidebar"]) .chat {
316
+ transition: none;
317
+ }
318
+ }
319
+
320
+ /* ── Launcher ───────────────────────────────────────────────────────────────
321
+ What a collapsed widget shrinks to: a round floating button by default, the
322
+ sidebar's slim edge rail under that placement. A sibling of the panel, so it
323
+ survives the panel being hidden, and the only part of a collapsed widget
324
+ that takes pointer events. */
325
+ .launcher {
326
+ display: flex;
327
+ position: absolute;
328
+ inset: var(--_launcher-inset);
329
+ align-items: center;
330
+ justify-content: center;
331
+ width: var(--_launcher-size);
332
+ height: var(--_launcher-size);
333
+ padding: 0;
301
334
  border: none;
335
+ border-radius: var(--_launcher-radius);
336
+ background: var(--_launcher-bg);
337
+ color: var(--_launcher-fg);
338
+ box-shadow: var(--_shadow);
302
339
  font: inherit;
340
+ cursor: pointer;
341
+ pointer-events: auto;
342
+ opacity: 0;
343
+ transform: scale(0.4);
344
+ visibility: hidden;
345
+ transition:
346
+ opacity var(--_motion) var(--_ease),
347
+ transform var(--_motion) var(--_ease-pop),
348
+ visibility var(--_motion) var(--_ease);
349
+ }
350
+
351
+ /* The launcher grows out of the corner the panel shrank into.
352
+ It is laid out at rest rather than display:none, which is what lets it
353
+ animate in *and* out: an element that was not rendered has no before-change
354
+ style to transition from, and one whose display flips to none cannot
355
+ transition at all. visibility keeps it unpaintable, untabbable and
356
+ unclickable in between -- the expanded panel's own controls sit under it and
357
+ must stay reachable. */
358
+ :host([collapsed]) .launcher {
359
+ opacity: 1;
360
+ transform: none;
361
+ visibility: visible;
303
362
  }
304
363
 
305
- :host([placement="sidebar"][collapsed]) .rail {
306
- display: flex;
364
+ :host([collapsed]) .launcher:hover {
365
+ transform: scale(1.06);
366
+ }
367
+
368
+ :host([collapsed]) .launcher:active {
369
+ transform: scale(0.94);
370
+ }
371
+
372
+ .launcher .icon-holder {
373
+ width: var(--_launcher-icon-size);
374
+ height: var(--_launcher-icon-size);
375
+ }
376
+
377
+ /* The unread badge rides the launcher's top-right corner. The ring in the
378
+ widget's own background is what separates it from the launcher underneath,
379
+ whatever colour the host themes either one. */
380
+ .launcher-badge {
307
381
  position: absolute;
382
+ top: -2px;
383
+ right: -2px;
384
+ display: inline-flex;
385
+ align-items: center;
386
+ justify-content: center;
387
+ box-sizing: border-box;
388
+ min-width: var(--_badge-size);
389
+ height: var(--_badge-size);
390
+ padding: 0 5px;
391
+ border-radius: 999px;
392
+ background: var(--_badge-bg);
393
+ color: var(--_badge-fg);
394
+ font-size: var(--_badge-font-size);
395
+ font-weight: 600;
396
+ line-height: 1;
397
+ box-shadow: 0 0 0 2px var(--_bg);
398
+ }
399
+
400
+ .launcher-badge[hidden] {
401
+ display: none;
402
+ }
403
+
404
+ /* The sidebar collapses to an edge rail instead: full height, square, flush
405
+ against the dock. It slides in with the panel rather than popping. */
406
+ :host([placement="sidebar"][collapsed]) .launcher {
308
407
  inset: 0;
408
+ width: auto;
409
+ height: auto;
309
410
  align-items: flex-start;
310
- justify-content: center;
311
411
  padding-top: 16px;
312
- border: 1px solid var(--ag-ui-border);
313
- background: var(--ag-ui-header-bg);
314
- color: var(--ag-ui-header-fg);
315
- cursor: pointer;
412
+ border: 1px solid var(--_border);
413
+ border-radius: 0;
414
+ background: var(--_header-bg);
415
+ color: var(--_header-fg);
416
+ box-shadow: none;
417
+ transform: none;
316
418
  }
317
419
 
420
+ /* Every transition in this file is timed by --_motion, so collapsing it to a
421
+ frame is the whole reduced-motion story: states still change (and display
422
+ still flips at the end of its discrete transition), nothing travels. */
318
423
  @media (prefers-reduced-motion: reduce) {
319
- :host([placement="sidebar"]),
320
- :host([placement="sidebar"]) .chat {
321
- transition: none;
424
+ :host {
425
+ --_motion: var(--ag-ui-motion, 0.001s);
322
426
  }
323
427
  }
324
428
 
325
429
  /* Embedded: drop the floating chrome and the high z-index stacking context so
326
430
  the widget lives in the host's own layout (fixes overlay/z-index clashes). */
327
431
  :host([placement="embedded"]) {
328
- --ag-ui-position: static;
329
- --ag-ui-width: 100%;
330
- --ag-ui-height: 100%;
331
- --ag-ui-max-width: 100%;
332
- --ag-ui-max-height: 100%;
333
- --ag-ui-shadow: none;
334
- --ag-ui-z-index: auto;
432
+ --_position: var(--ag-ui-position, static);
433
+ --_width: var(--ag-ui-width, 100%);
434
+ --_height: var(--ag-ui-height, 100%);
435
+ --_max-width: var(--ag-ui-max-width, 100%);
436
+ --_max-height: var(--ag-ui-max-height, 100%);
437
+ --_shadow: var(--ag-ui-shadow, none);
438
+ --_z-index: var(--ag-ui-z-index, auto);
335
439
  }
336
440
 
337
441
  .chat {
@@ -340,10 +444,10 @@ export const STYLES = `
340
444
  flex-direction: column;
341
445
  flex: 1;
342
446
  min-height: 0;
343
- background: var(--ag-ui-bg);
344
- border: 1px solid var(--ag-ui-border);
345
- border-radius: var(--ag-ui-radius);
346
- box-shadow: var(--ag-ui-shadow);
447
+ background: var(--_bg);
448
+ border: 1px solid var(--_border);
449
+ border-radius: var(--_radius);
450
+ box-shadow: var(--_shadow);
347
451
  overflow: hidden;
348
452
  }
349
453
 
@@ -353,9 +457,9 @@ export const STYLES = `
353
457
  justify-content: space-between;
354
458
  gap: 8px;
355
459
  padding: 10px 14px;
356
- border-bottom: 1px solid var(--ag-ui-border);
357
- background: var(--ag-ui-header-bg);
358
- color: var(--ag-ui-header-fg);
460
+ border-bottom: 1px solid var(--_border);
461
+ background: var(--_header-bg);
462
+ color: var(--_header-fg);
359
463
  }
360
464
 
361
465
  .header-title {
@@ -374,8 +478,8 @@ export const STYLES = `
374
478
  align-items: center;
375
479
  justify-content: center;
376
480
  flex: none;
377
- width: var(--ag-ui-icon-size, 22px);
378
- height: var(--ag-ui-icon-size, 22px);
481
+ width: var(--_icon-size);
482
+ height: var(--_icon-size);
379
483
  line-height: 1;
380
484
  }
381
485
 
@@ -383,7 +487,7 @@ export const STYLES = `
383
487
  width: 100%;
384
488
  height: 100%;
385
489
  object-fit: contain;
386
- border-radius: var(--ag-ui-icon-radius, 4px);
490
+ border-radius: var(--_icon-radius);
387
491
  }
388
492
 
389
493
  .header-controls {
@@ -409,36 +513,87 @@ export const STYLES = `
409
513
  background: rgba(255, 255, 255, 0.18);
410
514
  }
411
515
 
412
- /* Collapsed: keep just the header bar — hide the whole body, and let the host
413
- shrink to the header. (A host can also fully hide the element itself.) */
516
+ /* ── Collapse ───────────────────────────────────────────────────────────────
517
+ Collapsing shrinks the widget to the round floating launcher: the panel
518
+ scales down toward the launcher's corner and fades out, the launcher pops in
519
+ from that same point. Both halves are transform and opacity only, so the
520
+ morph runs on the compositor and never reflows the host page.
521
+
522
+ The host box keeps its expanded size. Animating it would animate layout, and
523
+ a dragged --ag-ui-width would then fight the launcher's own size. Nothing
524
+ paints there once the panel is gone, so the box only has to stop swallowing
525
+ clicks: pointer events go to none, and the launcher takes them back.
526
+
527
+ Two placements collapse to something else and restore what they need:
528
+ "sidebar" slides to its rail (below), while "embedded" and "page" keep the
529
+ original header bar — embedded is laid out by the host page, where a
530
+ floating circle would escape the layout, and page is a full-screen route
531
+ with no corner to float in. */
414
532
  :host([collapsed]) {
533
+ pointer-events: none;
534
+ }
535
+
536
+ :host([collapsed]) .chat {
537
+ opacity: 0;
538
+ transform: scale(0.94);
539
+ visibility: hidden;
540
+ }
541
+
542
+ /* visibility is what keeps the panel out of the tab order and the a11y tree at
543
+ rest without display:none killing the transition. It interpolates so that any
544
+ progress below 1 still counts as visible: the panel stays on screen for the
545
+ whole collapse and flips hidden exactly at the end, and on expand it is
546
+ visible from the first frame. */
547
+ .chat {
548
+ transform-origin: bottom right;
549
+ transition:
550
+ opacity var(--_motion) var(--_ease),
551
+ transform var(--_motion) var(--_ease),
552
+ visibility var(--_motion) var(--_ease);
553
+ }
554
+
555
+ :host([placement="bottom-left"]) .chat {
556
+ transform-origin: bottom left;
557
+ }
558
+
559
+ /* The two in-flow placements keep the original collapse: hide the body, let
560
+ the host shrink to the header bar. */
561
+ :host([collapsed]:is([placement="embedded"], [placement="page"])) {
415
562
  height: auto;
416
563
  max-height: none;
564
+ pointer-events: auto;
417
565
  }
418
566
 
419
- :host([collapsed]) .messages,
420
- :host([collapsed]) .input-row,
421
- :host([collapsed]) .skill-chips,
422
- :host([collapsed]) .skill-palette,
423
- :host([collapsed]) .skill-hint {
424
- display: none;
567
+ :host([collapsed]:is([placement="embedded"], [placement="page"])) .chat {
568
+ opacity: 1;
569
+ transform: none;
570
+ visibility: visible;
425
571
  }
426
572
 
427
- /* Edge-docked placements pin top *and* bottom, which would otherwise keep the
428
- panel full-height when collapsed; unpin the bottom so it shrinks to the
429
- header. (Floating/bottom-left pin only the bottom, so they already shrink.) */
430
- :host([collapsed][placement="side"]),
431
- :host([collapsed][placement="full"]) {
432
- bottom: auto;
573
+ /* These two keep the header bar, so the launcher must stay out of the way —
574
+ an embedded host is position: static, which would otherwise leave an
575
+ absolutely-positioned circle to escape the layout entirely and land against
576
+ whatever the page happens to position. */
577
+ :host([collapsed]:is([placement="embedded"], [placement="page"])) .launcher {
578
+ visibility: hidden;
579
+ opacity: 0;
580
+ }
581
+
582
+ :host([collapsed]:is([placement="embedded"], [placement="page"])) .messages,
583
+ :host([collapsed]:is([placement="embedded"], [placement="page"])) .input-row,
584
+ :host([collapsed]:is([placement="embedded"], [placement="page"])) .skill-chips,
585
+ :host([collapsed]:is([placement="embedded"], [placement="page"])) .skill-palette,
586
+ :host([collapsed]:is([placement="embedded"], [placement="page"])) .skill-hint {
587
+ display: none;
433
588
  }
434
589
 
435
590
  .messages {
436
591
  flex: 1;
437
592
  overflow-y: auto;
438
- padding: var(--ag-ui-pad);
593
+ padding: var(--_pad);
439
594
  display: flex;
440
595
  flex-direction: column;
441
- gap: var(--ag-ui-space);
596
+ gap: var(--_space);
442
597
  }
443
598
 
444
599
  /* Empty-state region (slot): centred while it's the only thing in the
@@ -446,7 +601,7 @@ export const STYLES = `
446
601
  .empty {
447
602
  margin: auto;
448
603
  text-align: center;
449
- color: var(--ag-ui-muted);
604
+ color: var(--_muted);
450
605
  }
451
606
 
452
607
  .empty[hidden] {
@@ -462,22 +617,22 @@ export const STYLES = `
462
617
  .answer {
463
618
  display: flex;
464
619
  flex-direction: column;
465
- gap: var(--ag-ui-space);
620
+ gap: var(--_space);
466
621
  align-self: stretch;
467
622
  min-width: 0;
468
623
  }
469
624
 
470
625
  :host([data-answer-well]) .answer {
471
- padding: var(--ag-ui-pad);
472
- background: var(--ag-ui-well-bg);
473
- border: 1px solid var(--ag-ui-well-border);
474
- border-radius: var(--ag-ui-radius);
626
+ padding: var(--_pad);
627
+ background: var(--_well-bg);
628
+ border: 1px solid var(--_well-border);
629
+ border-radius: var(--_radius);
475
630
  }
476
631
 
477
632
  .message {
478
633
  max-width: 80%;
479
- padding: var(--ag-ui-msg-pad);
480
- border-radius: var(--ag-ui-msg-radius);
634
+ padding: var(--_msg-pad);
635
+ border-radius: var(--_msg-radius);
481
636
  line-height: 1.4;
482
637
  white-space: pre-wrap;
483
638
  word-break: break-word;
@@ -515,14 +670,14 @@ export const STYLES = `
515
670
 
516
671
  .message--user {
517
672
  align-self: flex-end;
518
- background: var(--ag-ui-user-bg);
519
- color: var(--ag-ui-user-fg);
673
+ background: var(--_user-bg);
674
+ color: var(--_user-fg);
520
675
  border-bottom-right-radius: 4px;
521
676
  }
522
677
 
523
678
  .message--assistant {
524
679
  align-self: flex-start;
525
- background: var(--ag-ui-assistant-bg);
680
+ background: var(--_assistant-bg);
526
681
  border-bottom-left-radius: 4px;
527
682
  /* Assistant bubbles hold rendered markdown/HTML, so collapse the source
528
683
  whitespace the renderer leaves between block tags. */
@@ -552,7 +707,7 @@ export const STYLES = `
552
707
  }
553
708
 
554
709
  .message--assistant a {
555
- color: var(--ag-ui-accent);
710
+ color: var(--_accent);
556
711
  text-decoration: underline;
557
712
  }
558
713
 
@@ -567,8 +722,8 @@ export const STYLES = `
567
722
  .message--assistant pre {
568
723
  padding: 8px 10px;
569
724
  overflow: auto;
570
- background: var(--ag-ui-bg);
571
- border: 1px solid var(--ag-ui-border);
725
+ background: var(--_bg);
726
+ border: 1px solid var(--_border);
572
727
  border-radius: 6px;
573
728
  }
574
729
 
@@ -584,6 +739,13 @@ export const STYLES = `
584
739
  position: relative;
585
740
  }
586
741
 
742
+ /* This button is the only reader of --ag-ui-surface and --ag-ui-text, and
743
+ neither had a default until the alias layer gave them one. Both references
744
+ used to resolve to nothing, dropping the declaration: the background fell
745
+ back to the initial transparent, and the hover/copied colour to the
746
+ inherited body colour. The defaults chosen upstream (transparent, and the
747
+ body foreground) reproduce that, so the rename repaints nothing. A raised
748
+ surface behind the button is a separate design question. */
587
749
  .code-copy {
588
750
  position: absolute;
589
751
  top: 4px;
@@ -592,9 +754,9 @@ export const STYLES = `
592
754
  font: inherit;
593
755
  font-size: 0.75em;
594
756
  line-height: 1.6;
595
- color: var(--ag-ui-muted);
596
- background: var(--ag-ui-surface);
597
- border: 1px solid var(--ag-ui-border);
757
+ color: var(--_muted);
758
+ background: var(--_surface);
759
+ border: 1px solid var(--_border);
598
760
  border-radius: 4px;
599
761
  cursor: pointer;
600
762
  opacity: 0;
@@ -609,24 +771,24 @@ export const STYLES = `
609
771
  }
610
772
 
611
773
  .code-copy:hover {
612
- color: var(--ag-ui-text);
613
- background: var(--ag-ui-hover);
774
+ color: var(--_text);
775
+ background: var(--_hover);
614
776
  }
615
777
 
616
778
  .code-copy[data-state="copied"] {
617
779
  opacity: 1;
618
- color: var(--ag-ui-text);
780
+ color: var(--_text);
619
781
  }
620
782
 
621
783
  .code-copy[data-state="failed"] {
622
784
  opacity: 1;
623
- color: var(--ag-ui-danger, var(--ag-ui-text));
785
+ color: var(--_danger, var(--_text));
624
786
  }
625
787
 
626
788
  .message--assistant blockquote {
627
789
  padding-left: 10px;
628
- border-left: 3px solid var(--ag-ui-border);
629
- color: var(--ag-ui-muted);
790
+ border-left: 3px solid var(--_border);
791
+ color: var(--_muted);
630
792
  }
631
793
 
632
794
  /* Markdown tables. table/thead/tbody/tr/th/td are all in
@@ -647,13 +809,13 @@ export const STYLES = `
647
809
  .message--assistant th,
648
810
  .message--assistant td {
649
811
  padding: 6px 10px;
650
- border: 1px solid var(--ag-ui-border);
812
+ border: 1px solid var(--_border);
651
813
  text-align: left;
652
814
  vertical-align: top;
653
815
  }
654
816
 
655
817
  .message--assistant th {
656
- background: var(--ag-ui-hover);
818
+ background: var(--_hover);
657
819
  font-weight: 600;
658
820
  }
659
821
 
@@ -663,7 +825,7 @@ export const STYLES = `
663
825
  gap: 4px;
664
826
  align-items: center;
665
827
  padding: 12px 14px;
666
- background: var(--ag-ui-assistant-bg);
828
+ background: var(--_assistant-bg);
667
829
  border-radius: 14px;
668
830
  border-bottom-left-radius: 4px;
669
831
  }
@@ -672,7 +834,7 @@ export const STYLES = `
672
834
  width: 6px;
673
835
  height: 6px;
674
836
  border-radius: 50%;
675
- background: var(--ag-ui-muted);
837
+ background: var(--_muted);
676
838
  animation: ag-ui-pending 1.2s infinite ease-in-out both;
677
839
  }
678
840
 
@@ -705,7 +867,7 @@ export const STYLES = `
705
867
  flex-direction: column;
706
868
  gap: 4px;
707
869
  font-size: 12px;
708
- color: var(--ag-ui-muted);
870
+ color: var(--_muted);
709
871
  }
710
872
 
711
873
  .thoughts-toggle {
@@ -716,7 +878,7 @@ export const STYLES = `
716
878
  font: inherit;
717
879
  font-size: 12px;
718
880
  font-weight: 600;
719
- color: var(--ag-ui-muted);
881
+ color: var(--_muted);
720
882
  cursor: pointer;
721
883
  }
722
884
 
@@ -741,7 +903,7 @@ export const STYLES = `
741
903
  .thoughts-body {
742
904
  margin: 0;
743
905
  padding: 4px 0 4px 10px;
744
- border-left: 2px solid var(--ag-ui-border);
906
+ border-left: 2px solid var(--_border);
745
907
  max-height: 220px;
746
908
  overflow: auto;
747
909
  white-space: pre-wrap;
@@ -770,9 +932,9 @@ export const STYLES = `
770
932
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
771
933
  padding: 8px 10px;
772
934
  border-radius: 8px;
773
- background: var(--ag-ui-tool-bg);
774
- border: 1px solid var(--ag-ui-border);
775
- color: var(--ag-ui-tool-fg);
935
+ background: var(--_tool-bg);
936
+ border: 1px solid var(--_border);
937
+ color: var(--_tool-fg);
776
938
  }
777
939
 
778
940
  .tool-call-head {
@@ -805,10 +967,10 @@ export const STYLES = `
805
967
 
806
968
  /* Pending: a real spinning ring. Speed is tunable; reduced motion stops it. */
807
969
  .tool-call[data-status="pending"] .tool-call-icon {
808
- border: 2px solid var(--ag-ui-muted);
970
+ border: 2px solid var(--_muted);
809
971
  border-top-color: transparent;
810
972
  border-radius: 50%;
811
- animation: ag-ui-tool-spin var(--ag-ui-tool-spin-duration) linear infinite;
973
+ animation: ag-ui-tool-spin var(--_tool-spin-duration) linear infinite;
812
974
  }
813
975
 
814
976
  @keyframes ag-ui-tool-spin {
@@ -817,18 +979,18 @@ export const STYLES = `
817
979
 
818
980
  /* Settled: a themeable glyph coloured by outcome. */
819
981
  .tool-call[data-status="done"] .tool-call-icon::before {
820
- content: var(--ag-ui-tool-icon-done);
821
- color: var(--ag-ui-success);
982
+ content: var(--_tool-icon-done);
983
+ color: var(--_success);
822
984
  }
823
985
 
824
986
  .tool-call[data-status="error"] .tool-call-icon::before {
825
- content: var(--ag-ui-tool-icon-error);
826
- color: var(--ag-ui-danger);
987
+ content: var(--_tool-icon-error);
988
+ color: var(--_danger);
827
989
  }
828
990
 
829
991
  .tool-call[data-status="declined"] .tool-call-icon::before {
830
- content: var(--ag-ui-tool-icon-declined);
831
- color: var(--ag-ui-muted);
992
+ content: var(--_tool-icon-declined);
993
+ color: var(--_muted);
832
994
  }
833
995
 
834
996
  @media (prefers-reduced-motion: reduce) {
@@ -854,19 +1016,19 @@ export const STYLES = `
854
1016
  font-size: 11px;
855
1017
  font-weight: 600;
856
1018
  background: rgba(127, 127, 127, 0.16);
857
- color: var(--ag-ui-muted);
1019
+ color: var(--_muted);
858
1020
  }
859
1021
 
860
1022
  .tool-call[data-status="done"] .tool-call-status {
861
- color: var(--ag-ui-success);
1023
+ color: var(--_success);
862
1024
  }
863
1025
 
864
1026
  .tool-call[data-status="error"] .tool-call-status {
865
- color: var(--ag-ui-danger);
1027
+ color: var(--_danger);
866
1028
  }
867
1029
 
868
1030
  .tool-call[data-status="declined"] .tool-call-status {
869
- color: var(--ag-ui-muted);
1031
+ color: var(--_muted);
870
1032
  }
871
1033
 
872
1034
  .tool-call-body {
@@ -888,7 +1050,7 @@ export const STYLES = `
888
1050
  font-weight: 700;
889
1051
  letter-spacing: 0.06em;
890
1052
  text-transform: uppercase;
891
- color: var(--ag-ui-muted);
1053
+ color: var(--_muted);
892
1054
  }
893
1055
 
894
1056
  /* The record of a human decision on a gated call. An approved call used to
@@ -896,7 +1058,7 @@ export const STYLES = `
896
1058
  .skill-item-token {
897
1059
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
898
1060
  font-size: 0.92em;
899
- color: var(--ag-ui-accent);
1061
+ color: var(--_accent);
900
1062
  margin-right: 6px;
901
1063
  }
902
1064
 
@@ -904,7 +1066,7 @@ export const STYLES = `
904
1066
  flex: none;
905
1067
  font-size: 11px;
906
1068
  font-style: italic;
907
- color: var(--ag-ui-muted);
1069
+ color: var(--_muted);
908
1070
  }
909
1071
 
910
1072
  .tool-call-args,
@@ -913,12 +1075,12 @@ export const STYLES = `
913
1075
  padding: 6px 8px;
914
1076
  max-height: 160px;
915
1077
  overflow: auto;
916
- background: var(--ag-ui-bg);
917
- border: 1px solid var(--ag-ui-border);
1078
+ background: var(--_bg);
1079
+ border: 1px solid var(--_border);
918
1080
  border-radius: 6px;
919
1081
  white-space: pre-wrap;
920
1082
  word-break: break-word;
921
- color: var(--ag-ui-fg);
1083
+ color: var(--_fg);
922
1084
  }
923
1085
 
924
1086
  /* Display modes are pure visibility over one DOM shape, selected from the host
@@ -965,7 +1127,7 @@ export const STYLES = `
965
1127
  background: none;
966
1128
  font: inherit;
967
1129
  font-weight: 600;
968
- color: var(--ag-ui-accent);
1130
+ color: var(--_accent);
969
1131
  cursor: pointer;
970
1132
  }
971
1133
 
@@ -988,13 +1150,12 @@ export const STYLES = `
988
1150
  }
989
1151
 
990
1152
  .resize-handle:focus-visible {
991
- outline: 2px solid var(--ag-ui-accent);
1153
+ outline: 2px solid var(--_accent);
992
1154
  outline-offset: -2px;
993
1155
  }
994
1156
 
995
1157
  /* The grip sits on the corner opposite the panel's anchor, because a resize
996
- measures from whichever edge is not moving. Selected from the host attribute,
997
- so switching placement moves the grip immediately.
1158
+ measures from whichever edge is not moving.
998
1159
 
999
1160
  Default is floating: pinned bottom-right, so the grip is top-left. */
1000
1161
  .resize-handle {
@@ -1005,7 +1166,9 @@ export const STYLES = `
1005
1166
  cursor: nwse-resize;
1006
1167
  }
1007
1168
 
1008
- /* Embedded sits in normal flow, pinned top-left, so it grows bottom-right. */
1169
+ /* Placement is only the opening guess, and these two rules are all it is good
1170
+ for. Embedded sits in normal flow, pinned top-left, so it grows bottom-right;
1171
+ bottom-left is pinned there, so its free corner is top-right. */
1009
1172
  :host([placement="embedded"]) .resize-handle {
1010
1173
  top: auto;
1011
1174
  left: auto;
@@ -1014,16 +1177,64 @@ export const STYLES = `
1014
1177
  cursor: nwse-resize;
1015
1178
  }
1016
1179
 
1017
- /* Pinned bottom-left, so the free corner is top-right. */
1018
1180
  :host([placement="bottom-left"]) .resize-handle {
1019
1181
  left: auto;
1020
1182
  right: 0;
1021
1183
  cursor: nesw-resize;
1022
1184
  }
1023
1185
 
1024
- /* Docked: the placement owns the height, so only the inner edge is the user's. */
1186
+ /* Then the measurement corrects it. Which edges are held still belongs to the
1187
+ host's layout rather than to placement -- a floating panel a host right-aligns
1188
+ is anchored bottom-left, not bottom-right -- so the element measures the edges
1189
+ that stay put and stamps them here as "<y>-<x>". Equal specificity to the
1190
+ rules above, so source order is what lets the measured value win.
1191
+
1192
+ These must not be written as [data-resize-anchor~="left"]: the stamped value
1193
+ is one hyphenated token, and ~= matches whitespace-separated words, so such a
1194
+ selector cannot ever match. It did not, which left the grip drawn at the
1195
+ corner that moves while the cursor pointed along the right diagonal.
1196
+
1197
+ Each rule also sets both sides of its axis rather than only the side it
1198
+ moves. One that flipped a single way could not undo a placement guess that
1199
+ had flipped the other, which put the grip on the anchored corner for an
1200
+ embedded panel its host right-aligns. */
1201
+ :host([data-resize-anchor$="-left"]) .resize-handle {
1202
+ left: auto;
1203
+ right: 0;
1204
+ }
1205
+
1206
+ :host([data-resize-anchor$="-right"]) .resize-handle {
1207
+ left: 0;
1208
+ right: auto;
1209
+ }
1210
+
1211
+ :host([data-resize-anchor^="top-"]) .resize-handle {
1212
+ top: auto;
1213
+ bottom: 0;
1214
+ }
1215
+
1216
+ :host([data-resize-anchor^="bottom-"]) .resize-handle {
1217
+ top: 0;
1218
+ bottom: auto;
1219
+ }
1220
+
1221
+ /* One axis flipped means the drag runs along the other diagonal. */
1222
+ :host([data-resize-anchor="top-left"]) .resize-handle,
1223
+ :host([data-resize-anchor="bottom-right"]) .resize-handle {
1224
+ cursor: nwse-resize;
1225
+ }
1226
+
1227
+ :host([data-resize-anchor="top-right"]) .resize-handle,
1228
+ :host([data-resize-anchor="bottom-left"]) .resize-handle {
1229
+ cursor: nesw-resize;
1230
+ }
1231
+
1232
+ /* Docked: the placement owns the height, so only the inner edge is the user's --
1233
+ an edge, not a corner, so this outranks the measured anchor by coming after. */
1025
1234
  :host([placement="sidebar"]) .resize-handle,
1026
1235
  :host([placement="side"]) .resize-handle {
1236
+ top: 0;
1237
+ bottom: auto;
1027
1238
  width: 8px;
1028
1239
  height: 100%;
1029
1240
  cursor: ew-resize;
@@ -1042,106 +1253,191 @@ export const STYLES = `
1042
1253
  }
1043
1254
 
1044
1255
  .resize-handle[data-dragging] {
1045
- background: var(--ag-ui-accent);
1256
+ background: var(--_accent);
1046
1257
  opacity: 0.35;
1047
1258
  }
1048
1259
 
1260
+ /* ── Composer ───────────────────────────────────────────────────────────────
1261
+ One surface owns the border, the background and the focus ring; the field and
1262
+ its tool row sit inside it. The row used to be four siblings stretched to the
1263
+ textarea's height, which gave a paperclip the same visual weight as the field
1264
+ and turned Send into a full-height slab. */
1049
1265
  .input-row {
1050
1266
  display: flex;
1051
- gap: 8px;
1052
1267
  padding: 12px;
1053
- border-top: 1px solid var(--ag-ui-border);
1268
+ border-top: 1px solid var(--_border);
1054
1269
  }
1055
1270
 
1056
- .input {
1271
+ .composer {
1057
1272
  flex: 1;
1273
+ min-width: 0;
1274
+ display: flex;
1275
+ flex-direction: column;
1276
+ gap: 4px;
1277
+ padding: 6px 6px 6px 10px;
1278
+ background: var(--_input-bg);
1279
+ border: 1px solid var(--_border);
1280
+ border-radius: var(--_composer-radius);
1281
+ transition: border-color var(--_motion) var(--_ease);
1282
+ }
1283
+
1284
+ .composer:focus-within {
1285
+ border-color: var(--_accent);
1286
+ }
1287
+
1288
+ /* The field grows with its content (sized by #autoGrow) up to the ceiling,
1289
+ then scrolls. border-box keeps that measurement stable: with content-box the
1290
+ padding would be added to every scrollHeight read and the field would creep
1291
+ taller on each keystroke. */
1292
+ .input {
1293
+ box-sizing: border-box;
1058
1294
  resize: none;
1059
- background: var(--ag-ui-input-bg);
1060
- border: 1px solid var(--ag-ui-border);
1061
- border-radius: 8px;
1062
- padding: 8px 10px;
1295
+ max-height: var(--_composer-max-height);
1296
+ overflow-y: auto;
1297
+ padding: 6px 4px 2px;
1298
+ background: transparent;
1299
+ border: none;
1063
1300
  font: inherit;
1064
1301
  color: inherit;
1065
1302
  outline: none;
1066
1303
  }
1067
1304
 
1068
- .input:focus {
1069
- border-color: var(--ag-ui-accent);
1305
+ .composer-tools {
1306
+ display: flex;
1307
+ align-items: center;
1308
+ gap: 2px;
1070
1309
  }
1071
1310
 
1072
- .send {
1311
+ /* Icon buttons: quiet at rest, so the field is what the eye lands on. */
1312
+ .attach-btn,
1313
+ .voice-btn {
1314
+ display: inline-flex;
1315
+ align-items: center;
1316
+ justify-content: center;
1317
+ width: var(--_tool-btn-size);
1318
+ height: var(--_tool-btn-size);
1319
+ padding: 0;
1073
1320
  border: none;
1074
1321
  border-radius: 8px;
1075
- padding: 0 16px;
1076
- background: var(--ag-ui-accent);
1077
- color: #ffffff;
1322
+ background: transparent;
1323
+ color: var(--_muted);
1078
1324
  font: inherit;
1079
- font-weight: 600;
1325
+ line-height: 1;
1080
1326
  cursor: pointer;
1327
+ transition:
1328
+ background-color var(--_motion) var(--_ease),
1329
+ color var(--_motion) var(--_ease);
1081
1330
  }
1082
1331
 
1083
- .send:disabled {
1084
- opacity: 0.5;
1085
- cursor: default;
1332
+ .attach-btn:hover,
1333
+ .voice-btn:hover {
1334
+ background: var(--_hover);
1335
+ color: var(--_fg);
1086
1336
  }
1087
1337
 
1088
- /* The composer button doubles as the Stop control while a run is in flight. */
1089
- .send[data-state="running"] {
1090
- background: var(--ag-ui-muted);
1338
+ .attach-btn:disabled,
1339
+ .voice-btn:disabled {
1340
+ cursor: default;
1341
+ opacity: 0.6;
1091
1342
  }
1092
1343
 
1093
- /* ── File attachments ───────────────────────────────────────────────────── */
1094
- /* The 📎 picker button sits left of the input; hidden until data-attachments-url. */
1095
- .attach-btn {
1096
- border: 1px solid var(--ag-ui-border);
1097
- border-radius: 8px;
1098
- padding: 0 10px;
1099
- background: var(--ag-ui-input-bg);
1100
- color: inherit;
1344
+ /* Send closes the row on the right: a circle, the only filled control in the
1345
+ composer, so "the thing that acts" reads at a glance. */
1346
+ .send {
1347
+ margin-left: auto;
1348
+ display: inline-flex;
1349
+ align-items: center;
1350
+ justify-content: center;
1351
+ flex: none;
1352
+ width: var(--_send-size);
1353
+ height: var(--_send-size);
1354
+ padding: 0;
1355
+ border: none;
1356
+ border-radius: 50%;
1357
+ background: var(--_accent);
1358
+ color: #ffffff;
1101
1359
  font: inherit;
1360
+ line-height: 1;
1102
1361
  cursor: pointer;
1362
+ transition:
1363
+ background-color var(--_motion) var(--_ease),
1364
+ transform var(--_motion) var(--_ease-pop);
1103
1365
  }
1104
1366
 
1105
- .attach-btn:hover {
1106
- border-color: var(--ag-ui-accent);
1367
+ .send:hover {
1368
+ transform: scale(1.08);
1107
1369
  }
1108
1370
 
1109
- .attach-input {
1371
+ .send:active {
1372
+ transform: scale(0.92);
1373
+ }
1374
+
1375
+ .send:disabled {
1376
+ opacity: 0.5;
1377
+ cursor: default;
1378
+ transform: none;
1379
+ }
1380
+
1381
+ /* The composer button doubles as the Stop control while a run is in flight —
1382
+ same circle, different glyph, so nothing moves when a run starts. */
1383
+ .send[data-state="running"] {
1384
+ background: var(--_muted);
1385
+ }
1386
+
1387
+ .send[data-state="idle"] .send-stop,
1388
+ .send[data-state="running"] .send-send {
1110
1389
  display: none;
1111
1390
  }
1112
1391
 
1113
- /* The 🎤 mic button; shown only once #wireVoice mounts it. */
1114
- .voice-slot {
1115
- display: contents;
1392
+ /* Glyphs one class, painted from the button's own colour so every state
1393
+ (hover, recording, running) carries the icon with it. */
1394
+ .glyph {
1395
+ width: var(--_glyph-size);
1396
+ height: var(--_glyph-size);
1397
+ fill: none;
1398
+ stroke: currentColor;
1399
+ stroke-width: var(--_glyph-stroke);
1400
+ stroke-linecap: round;
1401
+ stroke-linejoin: round;
1116
1402
  }
1117
1403
 
1118
- .voice-btn {
1119
- border: 1px solid var(--ag-ui-border);
1120
- border-radius: 8px;
1121
- padding: 0 10px;
1122
- background: var(--ag-ui-input-bg);
1123
- color: inherit;
1124
- font: inherit;
1125
- cursor: pointer;
1404
+ .glyph--solid {
1405
+ fill: currentColor;
1406
+ stroke: none;
1126
1407
  }
1127
1408
 
1128
- .voice-btn:hover {
1129
- border-color: var(--ag-ui-accent);
1409
+ /* In an icon holder (header brand, launcher) the glyph takes the holder's size
1410
+ rather than the composer's. */
1411
+ .icon-holder .glyph {
1412
+ width: 100%;
1413
+ height: 100%;
1130
1414
  }
1131
1415
 
1132
- .voice-btn:disabled {
1133
- cursor: default;
1134
- opacity: 0.6;
1416
+ /* ── File attachments ───────────────────────────────────────────────────── */
1417
+ /* The picker button sits in the composer's tool row; hidden until
1418
+ data-attachments-url. */
1419
+ .attach-input {
1420
+ display: none;
1421
+ }
1422
+
1423
+ /* The mic button's mount point; filled only once #wireVoice mounts the
1424
+ control. */
1425
+ .voice-slot {
1426
+ display: contents;
1135
1427
  }
1136
1428
 
1137
1429
  /* Recording: a red tint + a gentle pulse so it's clearly "live". */
1138
1430
  .voice-btn[data-state="recording"] {
1139
- border-color: var(--ag-ui-danger);
1140
- background: var(--ag-ui-danger);
1431
+ background: var(--_danger);
1141
1432
  color: #ffffff;
1142
1433
  animation: ag-ui-voice-pulse 1.2s ease-in-out infinite;
1143
1434
  }
1144
1435
 
1436
+ .voice-btn[data-state="recording"]:hover {
1437
+ background: var(--_danger);
1438
+ color: #ffffff;
1439
+ }
1440
+
1145
1441
  @keyframes ag-ui-voice-pulse {
1146
1442
  0%, 100% { opacity: 1; }
1147
1443
  50% { opacity: 0.6; }
@@ -1180,10 +1476,10 @@ export const STYLES = `
1180
1476
  max-width: 100%;
1181
1477
  margin: 2px 0;
1182
1478
  padding: 3px 10px;
1183
- border: 1px dashed var(--ag-ui-border);
1479
+ border: 1px dashed var(--_border);
1184
1480
  border-radius: 999px;
1185
1481
  background: transparent;
1186
- color: var(--ag-ui-muted);
1482
+ color: var(--_muted);
1187
1483
  font-size: 0.8em;
1188
1484
  line-height: 1.4;
1189
1485
  }
@@ -1204,16 +1500,16 @@ export const STYLES = `
1204
1500
  gap: 6px;
1205
1501
  max-width: 100%;
1206
1502
  padding: 4px 8px;
1207
- border: 1px solid var(--ag-ui-border);
1503
+ border: 1px solid var(--_border);
1208
1504
  border-radius: 999px;
1209
- background: var(--ag-ui-assistant-bg);
1505
+ background: var(--_assistant-bg);
1210
1506
  font-size: 0.85em;
1211
1507
  position: relative;
1212
1508
  }
1213
1509
 
1214
1510
  .attachment-chip--error {
1215
- border-color: var(--ag-ui-danger);
1216
- color: var(--ag-ui-danger);
1511
+ border-color: var(--_danger);
1512
+ color: var(--_danger);
1217
1513
  }
1218
1514
 
1219
1515
  .attachment-chip-name {
@@ -1224,12 +1520,12 @@ export const STYLES = `
1224
1520
  }
1225
1521
 
1226
1522
  .attachment-chip-size {
1227
- color: var(--ag-ui-muted);
1523
+ color: var(--_muted);
1228
1524
  white-space: nowrap;
1229
1525
  }
1230
1526
 
1231
1527
  .attachment-chip--error .attachment-chip-size {
1232
- color: var(--ag-ui-danger);
1528
+ color: var(--_danger);
1233
1529
  }
1234
1530
 
1235
1531
  /* The progress bar fills as the file uploads. */
@@ -1237,13 +1533,13 @@ export const STYLES = `
1237
1533
  flex-basis: 100%;
1238
1534
  height: 3px;
1239
1535
  border-radius: 2px;
1240
- background: var(--ag-ui-border);
1536
+ background: var(--_border);
1241
1537
  overflow: hidden;
1242
1538
  }
1243
1539
 
1244
1540
  .attachment-chip-bar-fill {
1245
1541
  height: 100%;
1246
- background: var(--ag-ui-accent);
1542
+ background: var(--_accent);
1247
1543
  transition: width 0.15s ease;
1248
1544
  }
1249
1545
 
@@ -1265,14 +1561,14 @@ export const STYLES = `
1265
1561
 
1266
1562
  /* A subtle outline while a file is dragged over the shell. */
1267
1563
  .chat--dragover {
1268
- outline: 2px dashed var(--ag-ui-accent);
1564
+ outline: 2px dashed var(--_accent);
1269
1565
  outline-offset: -4px;
1270
1566
  }
1271
1567
 
1272
1568
  /* Muted "⏹ Stopped" line after a cancelled run — a note, not an error bubble. */
1273
1569
  .stopped-note {
1274
1570
  align-self: flex-start;
1275
- color: var(--ag-ui-muted);
1571
+ color: var(--_muted);
1276
1572
  font-size: 12px;
1277
1573
  padding: 2px 4px;
1278
1574
  }
@@ -1285,14 +1581,14 @@ export const STYLES = `
1285
1581
  flex-direction: column;
1286
1582
  gap: 8px;
1287
1583
  padding: 12px;
1288
- background: var(--ag-ui-bg);
1289
- border: 1px solid var(--ag-ui-accent);
1584
+ background: var(--_bg);
1585
+ border: 1px solid var(--_accent);
1290
1586
  border-radius: 10px;
1291
1587
  }
1292
1588
 
1293
1589
  .confirm[data-resolved] {
1294
1590
  opacity: 0.7;
1295
- border-color: var(--ag-ui-border);
1591
+ border-color: var(--_border);
1296
1592
  }
1297
1593
 
1298
1594
  .confirm-body {
@@ -1306,7 +1602,7 @@ export const STYLES = `
1306
1602
  overflow: auto;
1307
1603
  font-size: 12px;
1308
1604
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
1309
- background: var(--ag-ui-assistant-bg);
1605
+ background: var(--_assistant-bg);
1310
1606
  border-radius: 8px;
1311
1607
  white-space: pre-wrap;
1312
1608
  word-break: break-word;
@@ -1319,14 +1615,14 @@ export const STYLES = `
1319
1615
  }
1320
1616
 
1321
1617
  .confirm-btn {
1322
- border: 1px solid var(--ag-ui-border);
1618
+ border: 1px solid var(--_border);
1323
1619
  border-radius: 8px;
1324
1620
  padding: 8px 14px;
1325
1621
  font: inherit;
1326
1622
  font-weight: 600;
1327
1623
  cursor: pointer;
1328
- background: var(--ag-ui-bg);
1329
- color: var(--ag-ui-fg);
1624
+ background: var(--_bg);
1625
+ color: var(--_fg);
1330
1626
  }
1331
1627
 
1332
1628
  .confirm-btn:disabled {
@@ -1335,8 +1631,8 @@ export const STYLES = `
1335
1631
  }
1336
1632
 
1337
1633
  .confirm-btn--confirm {
1338
- border-color: var(--ag-ui-accent);
1339
- background: var(--ag-ui-accent);
1634
+ border-color: var(--_accent);
1635
+ background: var(--_accent);
1340
1636
  color: #ffffff;
1341
1637
  }
1342
1638
 
@@ -1348,14 +1644,14 @@ export const STYLES = `
1348
1644
  flex-direction: column;
1349
1645
  gap: 8px;
1350
1646
  padding: 12px;
1351
- background: var(--ag-ui-bg);
1352
- border: 1px solid var(--ag-ui-accent);
1647
+ background: var(--_bg);
1648
+ border: 1px solid var(--_accent);
1353
1649
  border-radius: 10px;
1354
1650
  }
1355
1651
 
1356
1652
  .approval[data-resolved] {
1357
1653
  opacity: 0.7;
1358
- border-color: var(--ag-ui-border);
1654
+ border-color: var(--_border);
1359
1655
  }
1360
1656
 
1361
1657
  .approval-body {
@@ -1369,14 +1665,14 @@ export const STYLES = `
1369
1665
  }
1370
1666
 
1371
1667
  .approval-btn {
1372
- border: 1px solid var(--ag-ui-border);
1668
+ border: 1px solid var(--_border);
1373
1669
  border-radius: 8px;
1374
1670
  padding: 8px 14px;
1375
1671
  font: inherit;
1376
1672
  font-weight: 600;
1377
1673
  cursor: pointer;
1378
- background: var(--ag-ui-bg);
1379
- color: var(--ag-ui-fg);
1674
+ background: var(--_bg);
1675
+ color: var(--_fg);
1380
1676
  }
1381
1677
 
1382
1678
  .approval-btn:disabled {
@@ -1385,8 +1681,8 @@ export const STYLES = `
1385
1681
  }
1386
1682
 
1387
1683
  .approval-btn--approve {
1388
- border-color: var(--ag-ui-accent);
1389
- background: var(--ag-ui-accent);
1684
+ border-color: var(--_accent);
1685
+ background: var(--_accent);
1390
1686
  color: #ffffff;
1391
1687
  }
1392
1688
 
@@ -1398,14 +1694,14 @@ export const STYLES = `
1398
1694
  flex-direction: column;
1399
1695
  gap: 8px;
1400
1696
  padding: 12px;
1401
- background: var(--ag-ui-bg);
1402
- border: 1px solid var(--ag-ui-accent);
1697
+ background: var(--_bg);
1698
+ border: 1px solid var(--_accent);
1403
1699
  border-radius: 10px;
1404
1700
  }
1405
1701
 
1406
1702
  .question[data-resolved] {
1407
1703
  opacity: 0.7;
1408
- border-color: var(--ag-ui-border);
1704
+ border-color: var(--_border);
1409
1705
  }
1410
1706
 
1411
1707
  .question-body {
@@ -1430,9 +1726,9 @@ export const STYLES = `
1430
1726
  width: 100%;
1431
1727
  padding: 8px 10px;
1432
1728
  font: inherit;
1433
- color: var(--ag-ui-fg);
1434
- background: var(--ag-ui-bg);
1435
- border: 1px solid var(--ag-ui-border);
1729
+ color: var(--_fg);
1730
+ background: var(--_bg);
1731
+ border: 1px solid var(--_border);
1436
1732
  border-radius: 8px;
1437
1733
  }
1438
1734
 
@@ -1446,13 +1742,13 @@ export const STYLES = `
1446
1742
  }
1447
1743
 
1448
1744
  .question-btn {
1449
- border: 1px solid var(--ag-ui-accent);
1745
+ border: 1px solid var(--_accent);
1450
1746
  border-radius: 8px;
1451
1747
  padding: 8px 14px;
1452
1748
  font: inherit;
1453
1749
  font-weight: 600;
1454
1750
  cursor: pointer;
1455
- background: var(--ag-ui-accent);
1751
+ background: var(--_accent);
1456
1752
  color: #ffffff;
1457
1753
  }
1458
1754
 
@@ -1470,18 +1766,18 @@ export const STYLES = `
1470
1766
  }
1471
1767
 
1472
1768
  .skill-chip {
1473
- border: 1px solid var(--ag-ui-border);
1769
+ border: 1px solid var(--_border);
1474
1770
  border-radius: 999px;
1475
1771
  padding: 4px 12px;
1476
1772
  font: inherit;
1477
1773
  font-size: 0.9em;
1478
1774
  cursor: pointer;
1479
- background: var(--ag-ui-assistant-bg);
1480
- color: var(--ag-ui-fg);
1775
+ background: var(--_assistant-bg);
1776
+ color: var(--_fg);
1481
1777
  }
1482
1778
 
1483
1779
  .skill-chip:hover {
1484
- border-color: var(--ag-ui-accent);
1780
+ border-color: var(--_accent);
1485
1781
  }
1486
1782
 
1487
1783
  .skill-palette {
@@ -1490,8 +1786,8 @@ export const STYLES = `
1490
1786
  flex-direction: column;
1491
1787
  max-height: 220px;
1492
1788
  overflow: auto;
1493
- background: var(--ag-ui-bg);
1494
- border: 1px solid var(--ag-ui-border);
1789
+ background: var(--_bg);
1790
+ border: 1px solid var(--_border);
1495
1791
  border-radius: 8px;
1496
1792
  box-shadow: 0 8px 24px rgba(20, 20, 50, 0.16);
1497
1793
  }
@@ -1507,11 +1803,11 @@ export const STYLES = `
1507
1803
  font: inherit;
1508
1804
  text-align: left;
1509
1805
  cursor: pointer;
1510
- color: var(--ag-ui-fg);
1806
+ color: var(--_fg);
1511
1807
  }
1512
1808
 
1513
1809
  .skill-item[aria-selected="true"] {
1514
- background: var(--ag-ui-assistant-bg);
1810
+ background: var(--_assistant-bg);
1515
1811
  }
1516
1812
 
1517
1813
  .skill-item-title {
@@ -1520,7 +1816,7 @@ export const STYLES = `
1520
1816
 
1521
1817
  .skill-item-desc {
1522
1818
  font-size: 0.85em;
1523
- color: var(--ag-ui-muted);
1819
+ color: var(--_muted);
1524
1820
  }
1525
1821
 
1526
1822
  /* The hint sits directly above the composer's top border, so a zero bottom
@@ -1529,19 +1825,38 @@ export const STYLES = `
1529
1825
  margin: 8px 12px;
1530
1826
  font-size: 0.85em;
1531
1827
  line-height: 1.4;
1532
- color: var(--ag-ui-danger);
1828
+ color: var(--_danger);
1533
1829
  }
1534
1830
 
1535
- /* Chat-history drawer — a slide-over within the chat panel. */
1831
+ /* Chat-history drawer — a slide-over within the chat panel.
1832
+ The hidden attribute stays the single source of truth for open/closed (no
1833
+ JS-driven animation state): display is transitioned discretely, so on close
1834
+ the drawer stays displayed for the whole exit and is removed only at the end,
1835
+ which is what lets the backdrop fade and the panel slide out. */
1536
1836
  .drawer {
1537
1837
  position: absolute;
1538
1838
  inset: 0;
1539
1839
  z-index: 5;
1540
1840
  display: flex;
1841
+ transition: visibility var(--_motion) var(--_ease);
1541
1842
  }
1542
1843
 
1844
+ /* Closed. The overlay keeps its box (display, not none) so the backdrop and
1845
+ panel inside it stay rendered and can transition both ways; visibility is
1846
+ what takes the whole subtree out of the tab order, the a11y tree and hit
1847
+ testing at rest, and it holds off until the slide has finished. */
1543
1848
  .drawer[hidden] {
1544
- display: none;
1849
+ display: flex;
1850
+ visibility: hidden;
1851
+ pointer-events: none;
1852
+ }
1853
+
1854
+ .drawer[hidden] .drawer-backdrop {
1855
+ opacity: 0;
1856
+ }
1857
+
1858
+ .drawer[hidden] .drawer-panel {
1859
+ transform: translateX(-100%);
1545
1860
  }
1546
1861
 
1547
1862
  .checkpoints {
@@ -1553,16 +1868,27 @@ export const STYLES = `
1553
1868
  flex-direction: column;
1554
1869
  gap: 0.25rem;
1555
1870
  padding: 0.5rem;
1556
- border: 1px solid var(--ag-ui-border);
1871
+ border: 1px solid var(--_border);
1557
1872
  border-radius: 0.5rem;
1558
- background: var(--ag-ui-assistant-bg);
1873
+ background: var(--_assistant-bg);
1559
1874
  box-shadow: 0 6px 24px rgb(0 0 0 / 12%);
1560
1875
  max-height: 60%;
1561
1876
  overflow-y: auto;
1877
+ transform-origin: top center;
1878
+ transition:
1879
+ opacity var(--_motion) var(--_ease),
1880
+ transform var(--_motion) var(--_ease),
1881
+ visibility var(--_motion) var(--_ease);
1562
1882
  }
1563
1883
 
1884
+ /* Same idiom as the drawer: laid out at rest, hidden by visibility, so the
1885
+ popover can animate open and closed. */
1564
1886
  .checkpoints[hidden] {
1565
- display: none;
1887
+ display: flex;
1888
+ visibility: hidden;
1889
+ pointer-events: none;
1890
+ opacity: 0;
1891
+ transform: scale(0.96) translateY(-6px);
1566
1892
  }
1567
1893
 
1568
1894
  .checkpoints-title {
@@ -1586,7 +1912,7 @@ export const STYLES = `
1586
1912
  }
1587
1913
 
1588
1914
  .checkpoint-row:hover {
1589
- background: var(--ag-ui-hover);
1915
+ background: var(--_hover);
1590
1916
  }
1591
1917
 
1592
1918
  .checkpoint-label {
@@ -1601,7 +1927,7 @@ export const STYLES = `
1601
1927
  font-size: 0.6875rem;
1602
1928
  padding: 0 0.375rem;
1603
1929
  border-radius: 999px;
1604
- background: var(--ag-ui-hover);
1930
+ background: var(--_hover);
1605
1931
  opacity: 0.8;
1606
1932
  }
1607
1933
 
@@ -1610,20 +1936,22 @@ export const STYLES = `
1610
1936
  font-size: 0.75rem;
1611
1937
  cursor: pointer;
1612
1938
  padding: 0.125rem 0.5rem;
1613
- border: 1px solid var(--ag-ui-border);
1939
+ border: 1px solid var(--_border);
1614
1940
  border-radius: 0.375rem;
1615
1941
  background: transparent;
1616
1942
  color: inherit;
1617
1943
  }
1618
1944
 
1619
1945
  .checkpoint-action:hover {
1620
- background: var(--ag-ui-hover);
1946
+ background: var(--_hover);
1621
1947
  }
1622
1948
 
1623
1949
  .drawer-backdrop {
1624
1950
  position: absolute;
1625
1951
  inset: 0;
1626
1952
  background: rgba(20, 20, 50, 0.32);
1953
+ opacity: 1;
1954
+ transition: opacity var(--_motion) var(--_ease);
1627
1955
  }
1628
1956
 
1629
1957
  .drawer-panel {
@@ -1632,19 +1960,21 @@ export const STYLES = `
1632
1960
  flex-direction: column;
1633
1961
  width: min(300px, 85%);
1634
1962
  height: 100%;
1635
- background: var(--ag-ui-bg);
1636
- border-right: 1px solid var(--ag-ui-border);
1637
- box-shadow: var(--ag-ui-shadow);
1963
+ background: var(--_bg);
1964
+ border-right: 1px solid var(--_border);
1965
+ box-shadow: var(--_shadow);
1638
1966
  overflow: hidden;
1967
+ transform: none;
1968
+ transition: transform var(--_motion) var(--_ease);
1639
1969
  }
1640
1970
 
1641
1971
  .drawer-header {
1642
1972
  display: flex;
1643
1973
  align-items: center;
1644
1974
  justify-content: space-between;
1645
- gap: var(--ag-ui-space);
1646
- padding: var(--ag-ui-pad);
1647
- border-bottom: 1px solid var(--ag-ui-border);
1975
+ gap: var(--_space);
1976
+ padding: var(--_pad);
1977
+ border-bottom: 1px solid var(--_border);
1648
1978
  }
1649
1979
 
1650
1980
  .drawer-title {
@@ -1652,10 +1982,10 @@ export const STYLES = `
1652
1982
  }
1653
1983
 
1654
1984
  .drawer-new {
1655
- border: 1px solid var(--ag-ui-border);
1656
- border-radius: var(--ag-ui-radius);
1657
- background: var(--ag-ui-bg);
1658
- color: var(--ag-ui-accent);
1985
+ border: 1px solid var(--_border);
1986
+ border-radius: var(--_radius);
1987
+ background: var(--_bg);
1988
+ color: var(--_accent);
1659
1989
  padding: 4px 10px;
1660
1990
  font: inherit;
1661
1991
  font-size: 0.85em;
@@ -1669,19 +1999,19 @@ export const STYLES = `
1669
1999
  }
1670
2000
 
1671
2001
  .drawer-empty {
1672
- padding: var(--ag-ui-pad);
2002
+ padding: var(--_pad);
1673
2003
  font-size: 0.9em;
1674
- color: var(--ag-ui-muted);
2004
+ color: var(--_muted);
1675
2005
  }
1676
2006
 
1677
2007
  .drawer-row {
1678
2008
  display: flex;
1679
2009
  align-items: stretch;
1680
- border-bottom: 1px solid var(--ag-ui-border);
2010
+ border-bottom: 1px solid var(--_border);
1681
2011
  }
1682
2012
 
1683
2013
  .drawer-row--active {
1684
- background: var(--ag-ui-assistant-bg);
2014
+ background: var(--_assistant-bg);
1685
2015
  }
1686
2016
 
1687
2017
  .drawer-row-select {
@@ -1708,12 +2038,12 @@ export const STYLES = `
1708
2038
 
1709
2039
  .drawer-row-time {
1710
2040
  font-size: 0.72em;
1711
- color: var(--ag-ui-muted);
2041
+ color: var(--_muted);
1712
2042
  }
1713
2043
 
1714
2044
  .drawer-row-preview {
1715
2045
  font-size: 0.8em;
1716
- color: var(--ag-ui-muted);
2046
+ color: var(--_muted);
1717
2047
  overflow: hidden;
1718
2048
  white-space: nowrap;
1719
2049
  text-overflow: ellipsis;
@@ -1730,7 +2060,7 @@ export const STYLES = `
1730
2060
  .drawer-row-delete {
1731
2061
  border: none;
1732
2062
  background: none;
1733
- color: var(--ag-ui-muted);
2063
+ color: var(--_muted);
1734
2064
  font-size: 0.9em;
1735
2065
  padding: 4px;
1736
2066
  cursor: pointer;
@@ -1741,10 +2071,10 @@ export const STYLES = `
1741
2071
  min-width: 0;
1742
2072
  margin: 6px 10px;
1743
2073
  padding: 4px 8px;
1744
- border: 1px solid var(--ag-ui-accent);
2074
+ border: 1px solid var(--_accent);
1745
2075
  border-radius: 6px;
1746
- background: var(--ag-ui-input-bg);
1747
- color: var(--ag-ui-fg);
2076
+ background: var(--_input-bg);
2077
+ color: var(--_fg);
1748
2078
  font: inherit;
1749
2079
  }
1750
2080
 
@@ -1757,13 +2087,13 @@ export const STYLES = `
1757
2087
  }
1758
2088
 
1759
2089
  .drawer-confirm-label {
1760
- color: var(--ag-ui-danger);
2090
+ color: var(--_danger);
1761
2091
  }
1762
2092
 
1763
2093
  .drawer-confirm-yes {
1764
2094
  border: none;
1765
2095
  border-radius: 6px;
1766
- background: var(--ag-ui-danger);
2096
+ background: var(--_danger);
1767
2097
  color: #ffffff;
1768
2098
  padding: 3px 10px;
1769
2099
  font: inherit;
@@ -1771,7 +2101,7 @@ export const STYLES = `
1771
2101
  }
1772
2102
 
1773
2103
  .drawer-confirm-no {
1774
- border: 1px solid var(--ag-ui-border);
2104
+ border: 1px solid var(--_border);
1775
2105
  border-radius: 6px;
1776
2106
  background: none;
1777
2107
  color: inherit;