@artooi/ag-ui-web-component 0.38.0 → 0.40.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 (142) hide show
  1. package/CHANGELOG.md +515 -1
  2. package/README.md +232 -56
  3. package/dist/ag-ui-web-component.bundle.js +370 -139
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +17 -14
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/activity_registry.d.ts +46 -0
  8. package/dist/core/activity_registry.d.ts.map +1 -0
  9. package/dist/core/ag_ui_chat.d.ts +23 -10
  10. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  11. package/dist/core/agui_client.d.ts +28 -0
  12. package/dist/core/agui_client.d.ts.map +1 -1
  13. package/dist/core/client_seed.d.ts +27 -0
  14. package/dist/core/client_seed.d.ts.map +1 -0
  15. package/dist/core/read_max_tool_rounds.d.ts +9 -0
  16. package/dist/core/read_max_tool_rounds.d.ts.map +1 -0
  17. package/dist/core/run_handlers.d.ts +80 -0
  18. package/dist/core/run_handlers.d.ts.map +1 -0
  19. package/dist/core/storage_scope.d.ts +110 -0
  20. package/dist/core/storage_scope.d.ts.map +1 -0
  21. package/dist/core/tool_outcome.d.ts +2 -3
  22. package/dist/core/tool_outcome.d.ts.map +1 -1
  23. package/dist/core/utils.d.ts +46 -0
  24. package/dist/core/utils.d.ts.map +1 -1
  25. package/dist/index.js +16268 -14747
  26. package/dist/index.js.map +4 -4
  27. package/dist/skills/skill_catalog.d.ts +67 -0
  28. package/dist/skills/skill_catalog.d.ts.map +1 -0
  29. package/dist/skills/skill_name_from.d.ts +12 -0
  30. package/dist/skills/skill_name_from.d.ts.map +1 -0
  31. package/dist/tools/tool_catalog.d.ts +79 -0
  32. package/dist/tools/tool_catalog.d.ts.map +1 -0
  33. package/dist/tools/tool_dispatch.d.ts +104 -0
  34. package/dist/tools/tool_dispatch.d.ts.map +1 -0
  35. package/dist/ui/composer/attachment_tray.d.ts.map +1 -1
  36. package/dist/ui/composer/auto_grow.d.ts +10 -0
  37. package/dist/ui/composer/auto_grow.d.ts.map +1 -0
  38. package/dist/ui/composer/composer_attachments.d.ts +57 -0
  39. package/dist/ui/composer/composer_attachments.d.ts.map +1 -0
  40. package/dist/ui/composer/composer_voice.d.ts +48 -0
  41. package/dist/ui/composer/composer_voice.d.ts.map +1 -0
  42. package/dist/ui/composer/voice_input.d.ts.map +1 -1
  43. package/dist/ui/excerpts/transcript_quote_offer.d.ts +57 -0
  44. package/dist/ui/excerpts/transcript_quote_offer.d.ts.map +1 -0
  45. package/dist/ui/fill_ui_string.d.ts +32 -0
  46. package/dist/ui/fill_ui_string.d.ts.map +1 -0
  47. package/dist/ui/history/conversation_history.d.ts +203 -0
  48. package/dist/ui/history/conversation_history.d.ts.map +1 -0
  49. package/dist/ui/history/relative_time.d.ts.map +1 -1
  50. package/dist/ui/history/thread_drawer.d.ts +0 -14
  51. package/dist/ui/history/thread_drawer.d.ts.map +1 -1
  52. package/dist/ui/interrupts/confirmation_card.d.ts.map +1 -1
  53. package/dist/ui/interrupts/pending_decision.d.ts +23 -0
  54. package/dist/ui/interrupts/pending_decision.d.ts.map +1 -0
  55. package/dist/ui/placement/is_collapsible_placement.d.ts +18 -0
  56. package/dist/ui/placement/is_collapsible_placement.d.ts.map +1 -0
  57. package/dist/ui/placement/is_draggable_placement.d.ts +9 -0
  58. package/dist/ui/placement/is_draggable_placement.d.ts.map +1 -0
  59. package/dist/ui/placement/launcher_drag.d.ts +6 -0
  60. package/dist/ui/placement/launcher_drag.d.ts.map +1 -1
  61. package/dist/ui/placement/panel_placement.d.ts +119 -0
  62. package/dist/ui/placement/panel_placement.d.ts.map +1 -0
  63. package/dist/ui/progress/run_announcer.d.ts +41 -0
  64. package/dist/ui/progress/run_announcer.d.ts.map +1 -0
  65. package/dist/ui/progress/subagent_progress.d.ts +58 -0
  66. package/dist/ui/progress/subagent_progress.d.ts.map +1 -0
  67. package/dist/ui/progress/tool_call_card.d.ts.map +1 -1
  68. package/dist/ui/shell/adopt_styles.d.ts +23 -0
  69. package/dist/ui/shell/adopt_styles.d.ts.map +1 -0
  70. package/dist/ui/shell/glyph_slot.d.ts +7 -0
  71. package/dist/ui/shell/glyph_slot.d.ts.map +1 -0
  72. package/dist/ui/shell/header_button.d.ts +10 -0
  73. package/dist/ui/shell/header_button.d.ts.map +1 -0
  74. package/dist/ui/shell/icon_element.d.ts +12 -0
  75. package/dist/ui/shell/icon_element.d.ts.map +1 -0
  76. package/dist/ui/shell/is_unread_badge_enabled.d.ts +10 -0
  77. package/dist/ui/shell/is_unread_badge_enabled.d.ts.map +1 -0
  78. package/dist/ui/shell/read_launcher_icon_url.d.ts +8 -0
  79. package/dist/ui/shell/read_launcher_icon_url.d.ts.map +1 -0
  80. package/dist/ui/styles.d.ts +1 -1
  81. package/dist/ui/styles.d.ts.map +1 -1
  82. package/dist/ui/transcript/answer_actions.d.ts +39 -0
  83. package/dist/ui/transcript/answer_actions.d.ts.map +1 -0
  84. package/dist/ui/transcript/answer_stream.d.ts +56 -0
  85. package/dist/ui/transcript/answer_stream.d.ts.map +1 -0
  86. package/dist/ui/transcript/render_or_warn.d.ts +16 -0
  87. package/dist/ui/transcript/render_or_warn.d.ts.map +1 -0
  88. package/dist/ui/transcript/starter_chips.d.ts +21 -0
  89. package/dist/ui/transcript/starter_chips.d.ts.map +1 -0
  90. package/dist/ui/transcript/transcript.d.ts +171 -0
  91. package/dist/ui/transcript/transcript.d.ts.map +1 -0
  92. package/dist/ui/ui_strings.d.ts +29 -1
  93. package/dist/ui/ui_strings.d.ts.map +1 -1
  94. package/package.json +1 -1
  95. package/src/constants.ts +17 -15
  96. package/src/core/activity_registry.ts +133 -0
  97. package/src/core/ag_ui_chat.ts +945 -4419
  98. package/src/core/agui_client.ts +172 -16
  99. package/src/core/client_seed.ts +27 -0
  100. package/src/core/read_max_tool_rounds.ts +13 -0
  101. package/src/core/remote_conversation_store.ts +1 -1
  102. package/src/core/run_handlers.ts +501 -0
  103. package/src/core/storage_scope.ts +279 -0
  104. package/src/core/tool_outcome.ts +5 -3
  105. package/src/core/utils.ts +109 -0
  106. package/src/skills/skill_catalog.ts +166 -0
  107. package/src/skills/skill_name_from.ts +19 -0
  108. package/src/tools/tool_catalog.ts +354 -0
  109. package/src/tools/tool_dispatch.ts +521 -0
  110. package/src/ui/composer/attachment_tray.ts +6 -5
  111. package/src/ui/composer/auto_grow.ts +12 -0
  112. package/src/ui/composer/composer_attachments.ts +324 -0
  113. package/src/ui/composer/composer_voice.ts +93 -0
  114. package/src/ui/composer/voice_input.ts +4 -4
  115. package/src/ui/excerpts/transcript_quote_offer.ts +198 -0
  116. package/src/ui/fill_ui_string.ts +41 -0
  117. package/src/ui/history/conversation_history.ts +699 -0
  118. package/src/ui/history/relative_time.ts +5 -4
  119. package/src/ui/history/thread_drawer.ts +3 -50
  120. package/src/ui/interrupts/confirmation_card.ts +4 -2
  121. package/src/ui/interrupts/pending_decision.ts +34 -0
  122. package/src/ui/placement/is_collapsible_placement.ts +19 -0
  123. package/src/ui/placement/is_draggable_placement.ts +23 -0
  124. package/src/ui/placement/launcher_drag.ts +104 -89
  125. package/src/ui/placement/panel_placement.ts +1130 -0
  126. package/src/ui/progress/run_announcer.ts +67 -0
  127. package/src/ui/progress/subagent_progress.ts +201 -0
  128. package/src/ui/progress/tool_call_card.ts +2 -0
  129. package/src/ui/shell/adopt_styles.ts +28 -0
  130. package/src/ui/shell/glyph_slot.ts +12 -0
  131. package/src/ui/shell/header_button.ts +21 -0
  132. package/src/ui/shell/icon_element.ts +33 -0
  133. package/src/ui/shell/is_unread_badge_enabled.ts +11 -0
  134. package/src/ui/shell/read_launcher_icon_url.ts +9 -0
  135. package/src/ui/styles.ts +301 -70
  136. package/src/ui/transcript/answer_actions.ts +139 -0
  137. package/src/ui/transcript/answer_stream.ts +123 -0
  138. package/src/ui/transcript/render_or_warn.ts +22 -0
  139. package/src/ui/transcript/starter_chips.ts +42 -0
  140. package/src/ui/transcript/transcript.ts +429 -0
  141. package/src/ui/ui_strings.ts +38 -1
  142. package/src/version.ts +1 -1
package/src/ui/styles.ts CHANGED
@@ -38,6 +38,12 @@ export const STYLES = `
38
38
  --_header-fg: var(--ag-ui-header-fg, #ffffff);
39
39
  --_border: var(--ag-ui-border, #e2e2ec);
40
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));
41
47
 
42
48
  /* Body text and raised chrome, read only by the code-block copy button.
43
49
  The defaults restate what it inherits, so a host that sets neither sees
@@ -84,6 +90,7 @@ export const STYLES = `
84
90
  --_tool-icon-done: var(--ag-ui-tool-icon-done, "✓");
85
91
  --_tool-icon-error: var(--ag-ui-tool-icon-error, "✕");
86
92
  --_tool-icon-declined: var(--ag-ui-tool-icon-declined, "⊘");
93
+ --_tool-icon-interrupted: var(--ag-ui-tool-icon-interrupted, "◌");
87
94
 
88
95
  /* Disclosure marks on every expandable row. Tokenised for the same reason
89
96
  the status icons above are: a host re-theming one set and not the other
@@ -95,6 +102,7 @@ export const STYLES = `
95
102
  /* Answer well (opt-in via data-answer-well) — boxes a whole assistant turn. */
96
103
  --_well-bg: var(--ag-ui-well-bg, transparent);
97
104
  --_well-border: var(--ag-ui-well-border, var(--_border));
105
+ --_well-radius: var(--ag-ui-well-radius, var(--_radius));
98
106
 
99
107
  /* Surface — set --ag-ui-shadow: none for a flush, embedded panel. */
100
108
  --_shadow: var(--ag-ui-shadow, 0 12px 32px rgba(20, 20, 50, 0.18));
@@ -172,7 +180,24 @@ export const STYLES = `
172
180
  keyboard changes no viewport-percentage length -- not vh, not dvh, not svh
173
181
  -- so a full-bleed panel on a phone has to be told the height rather than
174
182
  deriving it. The value to publish there is the visual viewport's. */
175
- --_viewport-height: var(--ag-ui-viewport-height, var(--_visual-viewport-height));
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. */
176
201
  /* The measured height of the part of the screen the user can actually see,
177
202
  written by the element from the visual viewport and falling back to the
178
203
  layout viewport where nothing has measured yet.
@@ -185,11 +210,15 @@ export const STYLES = `
185
210
 
186
211
  Separate from the token above so a host that states the usable height
187
212
  outright still wins: the element writes this one inline, and an inline
188
- value would otherwise outrank the host's own rule. */
189
- --_visual-viewport-height: var(
190
- --ag-ui-visual-viewport-height,
191
- calc(100vh - var(--_viewport-inset-top) - var(--_viewport-inset-bottom))
192
- );
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);
193
222
  /* How much of the layout viewport is hidden below the visible one, measured
194
223
  and written by the element alongside the height above.
195
224
 
@@ -204,6 +233,24 @@ export const STYLES = `
204
233
  A host wanting no keyboard lift at all sets --ag-ui-keyboard-inset: 0px. */
205
234
  --_keyboard-inset: var(--ag-ui-keyboard-inset, var(--_visual-viewport-inset-bottom));
206
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);
207
254
  --_viewport-width: var(
208
255
  --ag-ui-viewport-width,
209
256
  calc(100vw - var(--_viewport-inset-left) - var(--_viewport-inset-right))
@@ -254,11 +301,14 @@ export const STYLES = `
254
301
  --_max-height: var(--ag-ui-max-height, calc(var(--_viewport-height) - var(--_edge-gutter)));
255
302
  /* Reading-column width for placement="page" (full-bleed, centred content). */
256
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));
257
309
  /* Slim rail the sidebar placement collapses to. Only that placement reads
258
310
  it, but it is declared here so every alias has a default in one place. */
259
311
  --_rail-width: var(--ag-ui-rail-width, 52px);
260
- /* Width of the docked conversation list on a full-page chat. */
261
- --_threads-rail-width: var(--ag-ui-threads-rail-width, 280px);
262
312
 
263
313
  position: var(--_position);
264
314
  inset: var(--_inset);
@@ -337,23 +387,24 @@ export const STYLES = `
337
387
  }
338
388
 
339
389
  :host([placement="side"]) {
340
- --_inset: var(--ag-ui-inset, var(--_viewport-inset-top) var(--_viewport-inset-right) var(--_viewport-inset-bottom) auto);
390
+ --_inset: var(--ag-ui-inset, calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right) var(--_viewport-inset-bottom) auto);
341
391
  --_width: var(--ag-ui-width, 420px);
342
392
  --_height: var(--ag-ui-height, var(--_viewport-height));
343
393
  --_max-height: var(--ag-ui-max-height, var(--_viewport-height));
344
- --_radius: var(--ag-ui-radius, 0);
394
+ --_panel-radius: var(--ag-ui-panel-radius, 0);
345
395
  }
346
396
 
347
397
  :host([placement="full"]) {
348
398
  --_inset: var(
349
399
  --ag-ui-inset,
350
- var(--_viewport-inset-top) var(--_viewport-inset-right) var(--_viewport-inset-bottom) var(--_viewport-inset-left)
400
+ calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right)
401
+ var(--_viewport-inset-bottom) var(--_viewport-inset-left)
351
402
  );
352
403
  --_width: var(--ag-ui-width, var(--_viewport-width));
353
404
  --_height: var(--ag-ui-height, var(--_viewport-height));
354
405
  --_max-width: var(--ag-ui-max-width, var(--_viewport-width));
355
406
  --_max-height: var(--ag-ui-max-height, var(--_viewport-height));
356
- --_radius: var(--ag-ui-radius, 0);
407
+ --_panel-radius: var(--ag-ui-panel-radius, 0);
357
408
  }
358
409
 
359
410
  /* Page: full-bleed background with a centred reading column capped at
@@ -364,13 +415,14 @@ export const STYLES = `
364
415
  :host([placement="page"]) {
365
416
  --_inset: var(
366
417
  --ag-ui-inset,
367
- var(--_viewport-inset-top) var(--_viewport-inset-right) var(--_viewport-inset-bottom) var(--_viewport-inset-left)
418
+ calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right)
419
+ var(--_viewport-inset-bottom) var(--_viewport-inset-left)
368
420
  );
369
421
  --_width: var(--ag-ui-width, var(--_viewport-width));
370
422
  --_height: var(--ag-ui-height, var(--_viewport-height));
371
423
  --_max-width: var(--ag-ui-max-width, var(--_viewport-width));
372
424
  --_max-height: var(--ag-ui-max-height, var(--_viewport-height));
373
- --_radius: var(--ag-ui-radius, 0);
425
+ --_panel-radius: var(--ag-ui-panel-radius, 0);
374
426
  }
375
427
 
376
428
  :host([placement="page"]) .messages {
@@ -430,7 +482,7 @@ export const STYLES = `
430
482
  :host([placement=""]:not([data-small-viewport="off"])) {
431
483
  --_inset: var(
432
484
  --ag-ui-inset,
433
- var(--_viewport-inset-top) var(--_viewport-inset-right)
485
+ calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right)
434
486
  calc(var(--_viewport-inset-bottom) + var(--_keyboard-inset))
435
487
  var(--_viewport-inset-left)
436
488
  );
@@ -438,7 +490,7 @@ export const STYLES = `
438
490
  --_height: var(--ag-ui-height, var(--_viewport-height));
439
491
  --_max-width: var(--ag-ui-max-width, var(--_viewport-width));
440
492
  --_max-height: var(--ag-ui-max-height, var(--_viewport-height));
441
- --_radius: var(--ag-ui-radius, 0);
493
+ --_panel-radius: var(--ag-ui-panel-radius, 0);
442
494
  --_shadow: var(--ag-ui-shadow, none);
443
495
  }
444
496
 
@@ -460,16 +512,20 @@ export const STYLES = `
460
512
  --ag-ui-position: static (and place this element in your own layout) for a
461
513
  host-managed push instead. */
462
514
  :host([placement="sidebar"]) {
463
- --_inset: var(--ag-ui-inset, var(--_viewport-inset-top) var(--_viewport-inset-right) var(--_viewport-inset-bottom) auto);
515
+ --_inset: var(--ag-ui-inset, calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right) var(--_viewport-inset-bottom) auto);
464
516
  --_width: var(--ag-ui-width, 420px);
465
517
  --_height: var(--ag-ui-height, var(--_viewport-height));
466
518
  --_max-height: var(--ag-ui-max-height, var(--_viewport-height));
467
- --_radius: var(--ag-ui-radius, 0);
519
+ --_panel-radius: var(--ag-ui-panel-radius, 0);
468
520
  transition: width var(--_motion) var(--_ease);
469
521
  }
470
522
 
471
523
  :host([placement="sidebar"][data-side="left"]) {
472
- --_inset: var(--ag-ui-inset, var(--_viewport-inset-top) auto var(--_viewport-inset-bottom) var(--_viewport-inset-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
+ );
473
529
  }
474
530
 
475
531
  /* The docked panel is pinned to the edge it docks against rather than filling
@@ -742,7 +798,7 @@ export const STYLES = `
742
798
  min-height: 0;
743
799
  background: var(--_bg);
744
800
  border: 1px solid var(--_border);
745
- border-radius: var(--_radius);
801
+ border-radius: var(--_panel-radius);
746
802
  box-shadow: var(--_shadow);
747
803
  overflow: hidden;
748
804
  }
@@ -957,48 +1013,6 @@ export const STYLES = `
957
1013
  display: none;
958
1014
  }
959
1015
 
960
- /* A docked conversation list: beside the transcript rather than over it.
961
-
962
- A full-page chat is the one surface with width to spare, and covering the
963
- conversation to show the list of conversations is the wrong trade there --
964
- it hides the thing you are trying to get back to. Everywhere else the panel
965
- is a few hundred pixels wide and a docked list would leave a column of
966
- transcript narrower than the messages in it, so this is the only placement
967
- that gets it.
968
-
969
- The transcript is moved by padding on the shell rather than by making the
970
- list a flex sibling: the drawer is the last child of the panel, and no
971
- selector reaches backwards from it to the rows that have to shift. That is
972
- also why the state is stamped on the host. */
973
- :host([data-threads-docked]) .drawer {
974
- /* Not a scrim: the page behind it is still the user's to work in. */
975
- pointer-events: none;
976
- }
977
-
978
- :host([data-threads-docked]) .drawer-backdrop {
979
- display: none;
980
- }
981
-
982
- :host([data-threads-docked]) .drawer-panel {
983
- width: var(--_threads-rail-width);
984
- pointer-events: auto;
985
- border-inline-end: 1px solid var(--_border);
986
- box-shadow: none;
987
- }
988
-
989
- :host([data-threads-docked]) .messages,
990
- :host([data-threads-docked]) .input-row,
991
- :host([data-threads-docked]) .skill-chips,
992
- :host([data-threads-docked]) .attachment-tray {
993
- padding-inline-start: calc(
994
- var(--_threads-rail-width) + max(var(--_pad), (100% - var(--_content-max-width)) / 2)
995
- );
996
- }
997
-
998
- :host([data-threads-docked]) .header {
999
- padding-inline-start: calc(var(--_threads-rail-width) + var(--_pad));
1000
- }
1001
-
1002
1016
  /* The page placement has no collapsed state, so it offers no control for one.
1003
1017
  A dedicated route has no "away" to go to: shrinking it left a strip of
1004
1018
  application chrome fixed over a route that no longer had an owner, and this
@@ -1176,6 +1190,162 @@ export const STYLES = `
1176
1190
  display: none;
1177
1191
  }
1178
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
+
1179
1349
  /* ── Answer group / well ─────────────────────────────────────────
1180
1350
  One .answer per assistant turn wraps the streamed text, its tool cards,
1181
1351
  and the pending indicator so a whole answer reads (and can be boxed) as one
@@ -1194,7 +1364,7 @@ export const STYLES = `
1194
1364
  padding: var(--_pad);
1195
1365
  background: var(--_well-bg);
1196
1366
  border: 1px solid var(--_well-border);
1197
- border-radius: var(--_radius);
1367
+ border-radius: var(--_well-radius);
1198
1368
  }
1199
1369
 
1200
1370
  .message {
@@ -1591,6 +1761,13 @@ export const STYLES = `
1591
1761
  color: var(--_muted);
1592
1762
  }
1593
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
+
1594
1771
  @media (prefers-reduced-motion: reduce) {
1595
1772
  .tool-call[data-status="pending"] .tool-call-icon {
1596
1773
  animation: none;
@@ -1629,7 +1806,8 @@ export const STYLES = `
1629
1806
  color: var(--_danger);
1630
1807
  }
1631
1808
 
1632
- .tool-call[data-status="declined"] .tool-call-status {
1809
+ .tool-call[data-status="declined"] .tool-call-status,
1810
+ .tool-call[data-status="interrupted"] .tool-call-status {
1633
1811
  color: var(--_muted);
1634
1812
  }
1635
1813
 
@@ -1645,6 +1823,17 @@ export const STYLES = `
1645
1823
  gap: 3px;
1646
1824
  }
1647
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
+
1648
1837
  /* The heading that tells the two payloads apart. Without it the arguments and
1649
1838
  the result were one run of text and a reader had to guess the boundary. */
1650
1839
  .tool-call-section-label {
@@ -2315,8 +2504,8 @@ export const STYLES = `
2315
2504
  display: none;
2316
2505
  }
2317
2506
 
2318
- /* The mic button's mount point; filled only once #wireVoice mounts the
2319
- control. */
2507
+ /* The mic button's mount point; filled only once ComposerVoice.wire mounts
2508
+ the control, which it skips unless transcription is configured. */
2320
2509
  .voice-slot {
2321
2510
  display: contents;
2322
2511
  }
@@ -2993,6 +3182,15 @@ export const STYLES = `
2993
3182
  padding: 10px 12px;
2994
3183
  }
2995
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
+
2996
3194
  .skill-chip {
2997
3195
  border: 1px solid var(--_border);
2998
3196
  border-radius: 999px;
@@ -3050,6 +3248,14 @@ export const STYLES = `
3050
3248
  box-shadow: 0 8px 24px rgba(20, 20, 50, 0.16);
3051
3249
  }
3052
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
+
3053
3259
  .skill-item {
3054
3260
  display: flex;
3055
3261
  flex-direction: column;
@@ -3096,17 +3302,26 @@ export const STYLES = `
3096
3302
  inset: 0;
3097
3303
  z-index: 5;
3098
3304
  display: flex;
3099
- transition: visibility var(--_motion) var(--_ease);
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;
3100
3312
  }
3101
3313
 
3102
3314
  /* Closed. The overlay keeps its box (display, not none) so the backdrop and
3103
3315
  panel inside it stay rendered and can transition both ways; visibility is
3104
3316
  what takes the whole subtree out of the tab order, the a11y tree and hit
3105
- testing at rest, and it holds off until the slide has finished. */
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. */
3106
3320
  .drawer[hidden] {
3107
3321
  display: flex;
3108
3322
  visibility: hidden;
3109
3323
  pointer-events: none;
3324
+ transition: visibility 0s var(--_motion);
3110
3325
  }
3111
3326
 
3112
3327
  .drawer[hidden] .drawer-backdrop {
@@ -3135,20 +3350,29 @@ export const STYLES = `
3135
3350
  /* Scrolling past the end of this must not scroll the page behind it. */
3136
3351
  overscroll-behavior: contain;
3137
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. */
3138
3356
  transition:
3139
3357
  opacity var(--_motion) var(--_ease),
3140
3358
  transform var(--_motion) var(--_ease),
3141
- visibility var(--_motion) var(--_ease);
3359
+ visibility 0s;
3142
3360
  }
3143
3361
 
3144
3362
  /* Same idiom as the drawer: laid out at rest, hidden by visibility, so the
3145
- popover can animate open and closed. */
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. */
3146
3366
  .checkpoints[hidden] {
3147
3367
  display: flex;
3148
3368
  visibility: hidden;
3149
3369
  pointer-events: none;
3150
3370
  opacity: 0;
3151
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);
3152
3376
  }
3153
3377
 
3154
3378
  .checkpoints-title {
@@ -3346,6 +3570,13 @@ export const STYLES = `
3346
3570
  padding: 0 var(--_pad) var(--_space);
3347
3571
  }
3348
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
+
3349
3580
  .queued-chip {
3350
3581
  max-width: 100%;
3351
3582
  overflow: hidden;