@ciridae/ui 1.0.5 → 1.0.9

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 (147) hide show
  1. package/README.md +3 -2
  2. package/dist/chat/conversation-source.d.ts +1 -3
  3. package/dist/chat/message-text.js +6 -6
  4. package/dist/chat/use-chat-conversation.d.ts +7 -2
  5. package/dist/chat/use-chat-conversation.js +2 -1
  6. package/dist/components/activity-feed.d.ts +0 -1
  7. package/dist/components/activity-feed.js +6 -7
  8. package/dist/components/agent-work.d.ts +5 -1
  9. package/dist/components/agent-work.js +27 -13
  10. package/dist/components/app-frame.d.ts +3 -17
  11. package/dist/components/app-frame.js +11 -30
  12. package/dist/components/app-navigation.d.ts +5 -6
  13. package/dist/components/app-navigation.js +65 -40
  14. package/dist/components/badge.d.ts +0 -4
  15. package/dist/components/badge.js +17 -27
  16. package/dist/components/bulk-action-bar.js +2 -2
  17. package/dist/components/button.js +41 -50
  18. package/dist/components/calendar.js +5 -8
  19. package/dist/components/centered-page.js +4 -4
  20. package/dist/components/chart.js +3 -4
  21. package/dist/components/chat-client-source.js +55 -34
  22. package/dist/components/chat-page.js +1 -1
  23. package/dist/components/chat-sheet.d.ts +30 -20
  24. package/dist/components/chat-sheet.js +109 -51
  25. package/dist/components/chat.d.ts +27 -56
  26. package/dist/components/chat.js +151 -129
  27. package/dist/components/checkbox-item.js +4 -4
  28. package/dist/components/checkbox-list.js +6 -6
  29. package/dist/components/chip.js +2 -2
  30. package/dist/components/collapsible.js +3 -3
  31. package/dist/components/color-theme.d.ts +3 -6
  32. package/dist/components/color-theme.js +10 -11
  33. package/dist/components/column-settings.js +3 -3
  34. package/dist/components/command-menu.d.ts +1 -2
  35. package/dist/components/command-menu.js +56 -77
  36. package/dist/components/command-palette.js +5 -2
  37. package/dist/components/copyable-id.js +5 -5
  38. package/dist/components/data-display.d.ts +1 -7
  39. package/dist/components/data-display.js +32 -55
  40. package/dist/components/data-grid.d.ts +4 -7
  41. package/dist/components/data-grid.js +106 -90
  42. package/dist/components/date-range-picker.js +4 -5
  43. package/dist/components/document-viewer.js +23 -23
  44. package/dist/components/drawer.d.ts +2 -1
  45. package/dist/components/drawer.js +45 -20
  46. package/dist/components/dropzone.js +13 -13
  47. package/dist/components/editable-field.d.ts +2 -12
  48. package/dist/components/editable-field.js +13 -26
  49. package/dist/components/email-input.js +2 -3
  50. package/dist/components/environment-frame.js +7 -7
  51. package/dist/components/expandable-text.js +2 -2
  52. package/dist/components/feedback-screenshot.js +7 -7
  53. package/dist/components/feedback-sheet.js +8 -8
  54. package/dist/components/filter-bar.js +41 -27
  55. package/dist/components/form-controls.d.ts +3 -6
  56. package/dist/components/form-controls.js +15 -22
  57. package/dist/components/form-layout.d.ts +6 -6
  58. package/dist/components/form-layout.js +10 -16
  59. package/dist/components/glass-menu.d.ts +2 -0
  60. package/dist/components/glass-menu.js +14 -18
  61. package/dist/components/gradient-edge.d.ts +0 -1
  62. package/dist/components/gradient-edge.js +8 -9
  63. package/dist/components/icon.js +1 -1
  64. package/dist/components/input.js +18 -17
  65. package/dist/components/keycap.d.ts +2 -6
  66. package/dist/components/keycap.js +4 -12
  67. package/dist/components/layout.js +11 -10
  68. package/dist/components/lightbox.js +8 -8
  69. package/dist/components/link.js +1 -1
  70. package/dist/components/locale.d.ts +23 -7
  71. package/dist/components/locale.js +19 -7
  72. package/dist/components/log-entry.d.ts +2 -14
  73. package/dist/components/log-entry.js +11 -22
  74. package/dist/components/log-list.js +1 -1
  75. package/dist/components/mention-input.js +13 -14
  76. package/dist/components/menu.d.ts +7 -4
  77. package/dist/components/menu.js +23 -10
  78. package/dist/components/meter.d.ts +2 -3
  79. package/dist/components/meter.js +12 -16
  80. package/dist/components/navigation.d.ts +0 -7
  81. package/dist/components/navigation.js +3 -12
  82. package/dist/components/notification-tray.js +7 -7
  83. package/dist/components/overlays.d.ts +5 -2
  84. package/dist/components/overlays.js +24 -24
  85. package/dist/components/panel.js +3 -3
  86. package/dist/components/progress.js +2 -2
  87. package/dist/components/radio-card.d.ts +1 -1
  88. package/dist/components/radio-card.js +9 -9
  89. package/dist/components/record.d.ts +2 -4
  90. package/dist/components/record.js +19 -32
  91. package/dist/components/scroll-area.js +10 -10
  92. package/dist/components/segmented-bar.js +9 -9
  93. package/dist/components/select.d.ts +3 -5
  94. package/dist/components/select.js +26 -44
  95. package/dist/components/settings.js +1 -1
  96. package/dist/components/shell-skeleton.js +1 -1
  97. package/dist/components/shell.d.ts +1 -1
  98. package/dist/components/shell.js +69 -24
  99. package/dist/components/side-panel.js +3 -3
  100. package/dist/components/sidebar.d.ts +0 -5
  101. package/dist/components/sidebar.js +25 -33
  102. package/dist/components/single-line-text.js +3 -4
  103. package/dist/components/site-banner.js +4 -4
  104. package/dist/components/skeleton.js +19 -22
  105. package/dist/components/skeletons.d.ts +0 -3
  106. package/dist/components/skeletons.js +5 -10
  107. package/dist/components/stages.d.ts +6 -3
  108. package/dist/components/stages.js +77 -105
  109. package/dist/components/stat-card.d.ts +2 -5
  110. package/dist/components/stat-card.js +23 -31
  111. package/dist/components/status-banner.js +6 -6
  112. package/dist/components/status.js +3 -3
  113. package/dist/components/steps.js +2 -2
  114. package/dist/components/switch.js +1 -1
  115. package/dist/components/tabs.js +15 -18
  116. package/dist/components/theme-customizer.d.ts +5 -8
  117. package/dist/components/theme-customizer.js +9 -41
  118. package/dist/components/theme-toggle.d.ts +0 -1
  119. package/dist/components/theme-toggle.js +6 -11
  120. package/dist/components/toast.d.ts +1 -7
  121. package/dist/components/toast.js +19 -22
  122. package/dist/components/user-chip.js +8 -8
  123. package/dist/components/wizard.js +6 -4
  124. package/dist/preset.d.ts +1 -2
  125. package/dist/preset.js +39 -89
  126. package/dist/styled-system/css/conditions.mjs +1 -1
  127. package/dist/styled-system/css/css.mjs +1 -1
  128. package/dist/styled-system/styles.css +805 -1279
  129. package/dist/styled-system/tokens/index.mjs +74 -1926
  130. package/dist/styled-system/tokens/tokens.d.ts +18 -33
  131. package/dist/styled-system/types/conditions.d.ts +0 -74
  132. package/dist/styled-system/types/prop-type.d.ts +15 -40
  133. package/dist/styled-system/types/style-props.d.ts +705 -705
  134. package/dist/theme/color.d.ts +0 -1
  135. package/dist/theme/color.js +0 -16
  136. package/dist/theme/derive.d.ts +3 -3
  137. package/dist/theme/derive.js +9 -32
  138. package/dist/theme/index.d.ts +1 -2
  139. package/dist/theme/index.js +3 -5
  140. package/dist/theme/options.d.ts +2 -4
  141. package/dist/theme/options.js +0 -1
  142. package/dist/theme/{palettes.d.ts → palette.d.ts} +12 -23
  143. package/dist/theme/palette.js +127 -0
  144. package/dist/theme/tokens.d.ts +8 -13
  145. package/dist/theme/tokens.js +38 -92
  146. package/package.json +1 -1
  147. package/dist/theme/palettes.js +0 -254
@@ -14,17 +14,9 @@ import { LogGroupHeader } from "./log-entry.js";
14
14
  import { MentionInput } from "./mention-input.js";
15
15
  import { useResolvedFileUrl } from "./resolved-file-url.js";
16
16
  export { MessageText, splitSources, } from "../chat/message-text.js";
17
- /*
18
- * Three composers share one behaviour. `page` is the wide card of a chat page. `sheet` is the
19
- * composer at the foot of the agent sheet: one soft card holding the text, which grows line by
20
- * line to six before it scrolls, over a bar with the tools as bare icons on the left and a small
21
- * round send on the right; what goes with the message, such as files, waits as chips over the
22
- * text. `feed` is a comment field under an activity feed: a pill with its send inside, which
23
- * grows into a soft card past one line; Enter breaks the line there, and Cmd/Ctrl+Enter sends.
24
- */
25
17
  const pageComposer = css({
26
18
  display: "flex",
27
- width: "100%",
19
+ width: "full",
28
20
  flexDirection: "column",
29
21
  overflow: "hidden",
30
22
  maxWidth: "chat.composer",
@@ -38,9 +30,9 @@ const pageComposer = css({
38
30
  "& > [data-attachments]": { paddingInline: "page.inline", paddingTop: "page.inline" },
39
31
  });
40
32
  const pageInput = css({
41
- width: "100%",
33
+ width: "full",
42
34
  minHeight: "chat.input",
43
- maxHeight: "calc(10lh + 2 * token(spacing.page.inline))",
35
+ maxHeight: "[calc(10lh + 2 * token(spacing.page.inline))]",
44
36
  padding: "page.inline",
45
37
  fontSize: "body",
46
38
  lineHeight: "body",
@@ -50,7 +42,7 @@ const sheetComposer = css({
50
42
  display: "flex",
51
43
  flexDirection: "column",
52
44
  gap: "layout.contentGap",
53
- width: "100%",
45
+ width: "full",
54
46
  minWidth: "0",
55
47
  padding: "panel.default",
56
48
  border: "1px solid token(colors.border.control)",
@@ -58,17 +50,15 @@ const sheetComposer = css({
58
50
  background: "bg.field",
59
51
  _focusWithin: { borderColor: "border.focus", boxShadow: "focus" },
60
52
  });
61
- // Wrapped lines take the body's spacing. The margins give back the leading past one line of
62
- // type, so a one-line composer stays the kit's height, its text box exactly one em tall.
53
+ // Wrapped lines take the body's spacing. The margins give back the leading past one line of type, so a one-line composer stays the kit's height, its text box exactly one em tall.
63
54
  const sheetInput = css({
64
- maxHeight: "6lh",
65
- marginBlock: "calc((1em - 1lh) / 2)",
55
+ maxHeight: "[6lh]",
56
+ marginBlock: "[calc((1em - 1lh) / 2)]",
66
57
  fontSize: "body",
67
58
  fontWeight: "medium",
68
59
  lineHeight: "body",
69
60
  });
70
- // Under the text: the tools on the left, and on the right the actions and the send, a 26px
71
- // round, smaller than any other icon button.
61
+ // The send is a 26px round, smaller than any other icon button.
72
62
  const sheetBar = css({
73
63
  display: "flex",
74
64
  alignItems: "center",
@@ -87,7 +77,7 @@ const feedComposer = css({
87
77
  display: "flex",
88
78
  alignItems: "flex-end",
89
79
  gap: "layout.tightGap",
90
- width: "100%",
80
+ width: "full",
91
81
  minWidth: "0",
92
82
  });
93
83
  // The tools beside the feed's pill: as tall as its send, so they stay level with its last line.
@@ -96,7 +86,7 @@ const feedTools = css({
96
86
  flexShrink: "0",
97
87
  alignItems: "center",
98
88
  gap: "layout.tightGap",
99
- height: "calc(token(sizes.control.compact) + 2 * token(spacing.control.compactBlock))",
89
+ height: "[calc(token(sizes.control.compact) + 2 * token(spacing.control.compactBlock))]",
100
90
  });
101
91
  const feedColumn = css({ display: "grid", flex: "1", minWidth: "0", gap: "layout.tightGap" });
102
92
  // The pill's 1px stroke sits inside its padding, so a one-line pill is the kit's height: the send
@@ -106,13 +96,13 @@ const feedBox = css({
106
96
  alignItems: "flex-end",
107
97
  gap: "layout.controlGap",
108
98
  minWidth: "0",
109
- paddingBlock: "calc(token(spacing.control.compactBlock) - token(sizes.border.hairline))",
99
+ paddingBlock: "[calc(token(spacing.control.compactBlock) - token(sizes.border.hairline))]",
110
100
  paddingInlineStart: "control.compactInline",
111
101
  paddingInlineEnd: "control.compactBlock",
112
102
  border: "1px solid token(colors.border.control)",
113
103
  // Half the one-line height: a pill at one line, a soft card past it.
114
- borderRadius: "calc((token(sizes.control.compact) + 2 * token(spacing.control.compactBlock)) / 2)",
115
- background: "token(gradients.card)",
104
+ borderRadius: "[calc((token(sizes.control.compact) + 2 * token(spacing.control.compactBlock)) / 2)]",
105
+ background: "[token(gradients.card)]",
116
106
  _focusWithin: { borderColor: "border.focus", boxShadow: "focus" },
117
107
  "& > [data-actions]": {
118
108
  display: "flex",
@@ -125,17 +115,15 @@ const feedBox = css({
125
115
  height: "control.compact",
126
116
  },
127
117
  });
128
- // Each line keeps the one-line pill's rhythm, so the first wrap only adds a line. Wrapped lines
129
- // take the body's spacing, as the sheet's do.
118
+ // Each line keeps the one-line pill's rhythm, so the first wrap only adds a line.
130
119
  const feedInput = css({
131
120
  flex: "1",
132
- paddingBlock: "calc((token(sizes.control.compact) - 1lh) / 2)",
133
- maxHeight: "calc(5lh + token(sizes.control.compact))",
121
+ paddingBlock: "[calc((token(sizes.control.compact) - 1lh) / 2)]",
122
+ maxHeight: "[calc(5lh + token(sizes.control.compact))]",
134
123
  fontSize: "body",
135
124
  fontWeight: "medium",
136
125
  lineHeight: "body",
137
126
  });
138
- /* A tool in the sheet composer's bar: a bare icon, with no face of its own. */
139
127
  const composerTool = css({
140
128
  display: "inline-flex",
141
129
  padding: "none",
@@ -149,11 +137,7 @@ const composerTool = css({
149
137
  _disabled: { opacity: "disabled", cursor: "not-allowed" },
150
138
  "&[aria-pressed=true]": { color: "fg.primary" },
151
139
  });
152
- /*
153
- * Dictation's level meter, in the composer's bar while the host listens: a bar per sample,
154
- * newest last, spread across the room the bar leaves it. Bars move with no transition, since one
155
- * would blur the motion of a meter updated several times a second.
156
- */
140
+ /* Bars move with no transition, since one would blur the motion of a meter updated several times a second. */
157
141
  const dictationWaveform = css({
158
142
  display: "flex",
159
143
  flex: "1",
@@ -165,17 +149,16 @@ const dictationWaveform = css({
165
149
  overflow: "hidden",
166
150
  "& > span": {
167
151
  flex: "1",
168
- minWidth: "1px",
152
+ minWidth: "[1px]",
169
153
  // A spacing token, so it is read through token(); a sizes property would print its name.
170
- maxWidth: "token(spacing.layout.tightGap)",
171
- height: "100%",
154
+ maxWidth: "[token(spacing.layout.tightGap)]",
155
+ height: "full",
172
156
  borderRadius: "round",
173
157
  background: "fg.subtle",
174
158
  transformOrigin: "center",
175
159
  },
176
160
  "&[data-listening] > span": { background: "fg.secondary" },
177
161
  });
178
- /** How short a silent bar of the dictation waveform is, as a share of its full height. */
179
162
  const silentWaveformLevel = 0.12;
180
163
  const composerFooter = css({
181
164
  display: "flex",
@@ -198,10 +181,10 @@ const actionGroup = css({
198
181
  gap: "control.iconGap",
199
182
  flexWrap: "wrap",
200
183
  _phone: {
201
- maxWidth: "100%",
184
+ maxWidth: "full",
202
185
  flexWrap: "nowrap",
203
186
  overflowX: "auto",
204
- scrollbarWidth: "none",
187
+ scrollbarWidth: "[none]",
205
188
  },
206
189
  });
207
190
  const chatAction = css({
@@ -222,9 +205,8 @@ const chatAction = css({
222
205
  _hover: { color: "fg.primary", borderColor: "fg.muted" },
223
206
  });
224
207
  /*
225
- * A message bubble: the user's on the right in the selected neutral face, under its gradient edge;
226
- * another author's on the left in the neutral fill. Where a bubble joins the one above or below
227
- * it on its side, that corner is small, so a run of messages reads as one stack.
208
+ * Where a bubble joins the one above or below it on its side, that corner is small, so a run of
209
+ * messages reads as one stack.
228
210
  */
229
211
  const bubble = cva({
230
212
  base: {
@@ -245,11 +227,11 @@ const bubble = cva({
245
227
  // Wrapped lines take the body's spacing. The kit sets one line at the type's own height
246
228
  // with its 1px stroke in the layout, which the edge's transparent border already counts,
247
229
  // so the padding gives back the extra leading and a one-line bubble stays the kit's 36px.
248
- paddingBlock: "calc(token(spacing.field.block) - (1lh - 1em) / 2)",
230
+ paddingBlock: "[calc(token(spacing.field.block) - (1lh - 1em) / 2)]",
249
231
  lineHeight: "body",
250
- background: "token(gradients.selected)",
232
+ background: "[token(gradients.selected)]",
251
233
  whiteSpace: "pre-wrap",
252
- "&::before": { background: "token(gradients.border.selected)" },
234
+ "&::before": { background: "[token(gradients.border.selected)]" },
253
235
  "&[data-group-position=first], &[data-group-position=middle]": {
254
236
  borderEndEndRadius: "keycap",
255
237
  },
@@ -270,11 +252,10 @@ const bubble = cva({
270
252
  },
271
253
  },
272
254
  });
273
- /* An answer in a thread of `plain` replies: text on the sheet itself, the thread's full width. */
274
255
  const plainReply = css({
275
256
  display: "grid",
276
257
  gap: "layout.contentGap",
277
- width: "100%",
258
+ width: "full",
278
259
  minWidth: "0",
279
260
  color: "fg.primary",
280
261
  fontSize: "body",
@@ -284,7 +265,7 @@ const plainReply = css({
284
265
  /** A line about the conversation rather than in it, centered between bubbles. */
285
266
  const systemLine = css({
286
267
  alignSelf: "center",
287
- maxWidth: "85%",
268
+ maxWidth: "[85%]",
288
269
  color: "fg.muted",
289
270
  fontSize: "code",
290
271
  lineHeight: "compact",
@@ -294,15 +275,13 @@ const systemLine = css({
294
275
  });
295
276
  /*
296
277
  * The thread: bubbles 4px apart and groups 12px apart, lines centered between them. Compact
297
- * keeps 8px above and below and between groups. Plain replies sit 16px in from the sheet's
298
- * edges, an answer's parts 12px apart and one turn 16px from the next.
278
+ * keeps 8px above and below and between groups.
299
279
  */
300
280
  const thread = css({
301
281
  display: "flex",
302
282
  flexDirection: "column",
303
283
  gap: "layout.tightGap",
304
284
  padding: "panel.default",
305
- // Every line in the thread is set in the medium weight: bubbles, replies, cited cards and meta.
306
285
  fontWeight: "medium",
307
286
  // The thread follows its newest entry itself: the browser's scroll anchoring would hold the
308
287
  // view on what was there as a reply lands and pull it off the foot.
@@ -330,18 +309,21 @@ const thread = css({
330
309
  const latestSlot = css({
331
310
  position: "sticky",
332
311
  bottom: "layout.controlGap",
333
- zIndex: "1",
312
+ zIndex: "[1]",
334
313
  display: "flex",
335
314
  alignItems: "flex-end",
336
315
  justifyContent: "center",
337
316
  height: "0",
338
- marginTop: "calc(token(spacing.layout.tightGap) * -1)",
317
+ marginTop: "[calc(token(spacing.layout.tightGap) * -1)]",
339
318
  pointerEvents: "none",
340
319
  "& > *": { pointerEvents: "auto", boxShadow: "card" },
341
320
  });
342
321
  /** How close to the bottom the reader must be for new content to keep the thread at its foot. */
343
322
  const followDistancePx = 96;
344
- /* A reply's sources under it: small links in a row, and the ones with a quote as cards. */
323
+ /* The thread's one live region, which reads each finished reply aloud to a screen reader. */
324
+ const visuallyHidden = css({ srOnly: true });
325
+ /** Past about this many characters an announced reply is cut, so a long one does not hold the reader. */
326
+ const announcedReplyMaximumLength = 300;
345
327
  const sourceRow = css({
346
328
  display: "flex",
347
329
  flexWrap: "wrap",
@@ -353,7 +335,7 @@ const sourceRow = css({
353
335
  fontSize: "code",
354
336
  lineHeight: "compact",
355
337
  "[data-avatar] > &": {
356
- marginInlineStart: "calc(token(sizes.avatar.small) + token(spacing.control.iconGap))",
338
+ marginInlineStart: "[calc(token(sizes.avatar.small) + token(spacing.control.iconGap))]",
357
339
  },
358
340
  "[data-reply-style=plain] > * > &": { paddingInline: "none" },
359
341
  "& a": { color: "fg.secondary", textDecoration: "none", _hover: { color: "fg.primary" } },
@@ -362,26 +344,22 @@ const sourceCardList = css({
362
344
  display: "grid",
363
345
  gap: "layout.contentGap",
364
346
  "[data-avatar] > &": {
365
- marginInlineStart: "calc(token(sizes.avatar.small) + token(spacing.control.iconGap))",
347
+ marginInlineStart: "[calc(token(sizes.avatar.small) + token(spacing.control.iconGap))]",
366
348
  },
367
349
  });
368
- /*
369
- * A source a reply cites, as a card: what it is under its icon, what it says, and when or by
370
- * whom. `yellow` marks a rule or a value the reply weighs, on the warm wash.
371
- */
372
350
  const sourceCard = css({
373
351
  display: "grid",
374
352
  gap: "control.iconGap",
375
353
  padding: "panel.default",
376
354
  border: "1px solid token(colors.border.divider)",
377
355
  borderRadius: "action",
378
- background: "token(gradients.card)",
356
+ background: "[token(gradients.card)]",
379
357
  color: "fg.primary",
380
358
  fontSize: "code",
381
359
  fontWeight: "medium",
382
360
  lineHeight: "log",
383
361
  textDecoration: "none",
384
- "&[data-tone=yellow]": { background: "bg.warningTint" },
362
+ "&[data-tone=warning]": { background: "bg.warningTint" },
385
363
  "& > [data-head]": {
386
364
  display: "flex",
387
365
  alignItems: "center",
@@ -391,28 +369,26 @@ const sourceCard = css({
391
369
  lineHeight: "tight",
392
370
  "& > svg": { flexShrink: "0", fontSize: "icon.small" },
393
371
  },
394
- "&[data-tone=yellow] > [data-head]": { color: "accent.yellowIcon" },
372
+ "&[data-tone=warning] > [data-head]": { color: "accent.yellowIcon" },
395
373
  "& > [data-meta]": { color: "fg.subtle", lineHeight: "tight" },
396
374
  "&:is(a)": { _hover: { borderColor: "border.selected" }, _focusVisible: { focusRing: "outer" } },
397
375
  });
398
- /* Buttons under a reply for what it suggests doing, always shown. */
399
376
  const suggestedActionRow = css({
400
377
  display: "flex",
401
378
  flexWrap: "wrap",
402
379
  alignItems: "center",
403
380
  gap: "layout.tightGap",
404
381
  "[data-avatar] > &": {
405
- marginInlineStart: "calc(token(sizes.avatar.small) + token(spacing.control.iconGap))",
382
+ marginInlineStart: "[calc(token(sizes.avatar.small) + token(spacing.control.iconGap))]",
406
383
  },
407
384
  });
408
- /* A reply's closing line, such as what it read, quieter than the reply. */
409
385
  const footnote = css({
410
386
  margin: "none",
411
387
  color: "fg.subtle",
412
388
  fontSize: "code",
413
389
  lineHeight: "log",
414
390
  "[data-avatar] > &": {
415
- marginInlineStart: "calc(token(sizes.avatar.small) + token(spacing.control.iconGap))",
391
+ marginInlineStart: "[calc(token(sizes.avatar.small) + token(spacing.control.iconGap))]",
416
392
  },
417
393
  });
418
394
  /* Files sent with a message, as tiles over its bubble: image thumbnails, or a file's icon and name. */
@@ -423,7 +399,7 @@ const attachmentTiles = css({
423
399
  maxWidth: "chat.bubble",
424
400
  "[data-side=user] > &": { justifyContent: "flex-end" },
425
401
  "[data-avatar] > &": {
426
- marginInlineStart: "calc(token(sizes.avatar.small) + token(spacing.control.iconGap))",
402
+ marginInlineStart: "[calc(token(sizes.avatar.small) + token(spacing.control.iconGap))]",
427
403
  },
428
404
  });
429
405
  /* Prompts to start from, stacked flush over the composer of an empty conversation. */
@@ -433,14 +409,16 @@ const suggestionList = css({
433
409
  alignItems: "flex-start",
434
410
  justifyContent: "flex-end",
435
411
  gap: "layout.tightGap",
436
- minHeight: "100%",
412
+ minHeight: "full",
437
413
  padding: "panel.default",
438
414
  });
439
415
  const suggestion = css({
440
416
  display: "inline-flex",
441
417
  alignItems: "center",
442
- maxWidth: "100%",
443
- height: "control.small",
418
+ maxWidth: "full",
419
+ // A prompt longer than the row wraps inside the chip, which grows to hold it.
420
+ minHeight: "control.small",
421
+ paddingBlock: "control.compactBlock",
444
422
  paddingInline: "control.compactInline",
445
423
  border: "1px solid token(colors.border.control)",
446
424
  borderRadius: "action",
@@ -464,7 +442,6 @@ const entryBlock = css({
464
442
  "&[data-group-position=single], &[data-group-position=first]": {
465
443
  marginTop: "layout.controlGap",
466
444
  },
467
- // A plain reply's parts, such as its cards and buttons under its text, sit 12px apart.
468
445
  "&[data-side=assistant]": { "[data-reply-style=plain] > &": { gap: "layout.contentGap" } },
469
446
  // A message's own controls, such as Copy, show while it is hovered or focused.
470
447
  "& [data-entry-actions]": {
@@ -472,18 +449,19 @@ const entryBlock = css({
472
449
  alignItems: "center",
473
450
  alignSelf: "center",
474
451
  flexShrink: "0",
475
- opacity: "0",
476
- transition: "opacity 120ms ease",
452
+ opacity: "[0]",
453
+ transitionProperty: "[opacity]",
454
+ transitionDuration: "fast",
477
455
  },
478
- "&:hover [data-entry-actions], &:focus-within [data-entry-actions]": { opacity: "1" },
479
- "@media (hover: none)": { "& [data-entry-actions]": { opacity: "1" } },
456
+ "&:hover [data-entry-actions], &:focus-within [data-entry-actions]": { opacity: "[1]" },
457
+ "@media (hover: none)": { "& [data-entry-actions]": { opacity: "[1]" } },
480
458
  });
481
459
  // Full width, so a bubble's maximum is a share of the thread rather than of its own text.
482
460
  const entryRow = css({
483
461
  display: "flex",
484
462
  alignItems: "flex-end",
485
463
  gap: "control.iconGap",
486
- width: "100%",
464
+ width: "full",
487
465
  minWidth: "0",
488
466
  "[data-side=user] > &": { justifyContent: "flex-end" },
489
467
  // A plain reply's Copy sits at the top of its text, which may run long.
@@ -500,7 +478,7 @@ const entryFooter = css({
500
478
  lineHeight: "compact",
501
479
  fontVariantNumeric: "tabular-nums",
502
480
  "[data-avatar] > &": {
503
- marginInlineStart: "calc(token(sizes.avatar.small) + token(spacing.control.iconGap))",
481
+ marginInlineStart: "[calc(token(sizes.avatar.small) + token(spacing.control.iconGap))]",
504
482
  },
505
483
  });
506
484
  const attachmentList = css({
@@ -508,18 +486,15 @@ const attachmentList = css({
508
486
  flexWrap: "wrap",
509
487
  gap: "layout.tightGap",
510
488
  marginBlockStart: "layout.tightGap",
511
- opacity: "0.8",
489
+ opacity: "[0.8]",
512
490
  fontSize: "code",
513
- "& a": { color: "inherit" },
491
+ "& a": { color: "[inherit]" },
514
492
  });
515
- /*
516
- * The agent at work, before its reply: what it is doing beside a spinner, over three lines of
517
- * the reply to come. The lines stand in for the reply's text, so each is that text's bar height.
518
- */
493
+ /* The lines stand in for the reply's text, so each is that text's bar height. */
519
494
  const thinking = css({
520
495
  display: "grid",
521
496
  gap: "layout.contentGap",
522
- width: "100%",
497
+ width: "full",
523
498
  minWidth: "0",
524
499
  "& > [data-status]": {
525
500
  display: "flex",
@@ -533,31 +508,30 @@ const thinking = css({
533
508
  },
534
509
  "& > [data-line]": {
535
510
  display: "block",
536
- height: "calc(token(fontSizes.body) * 0.75)",
511
+ height: "[calc(token(fontSizes.body) * 0.75)]",
537
512
  borderRadius: "round",
538
- background: "token(gradients.skeletonFill)",
513
+ background: "[token(gradients.skeletonFill)]",
539
514
  },
540
- "& > [data-line]:nth-of-type(1)": { width: "98%" },
541
- "& > [data-line]:nth-of-type(2)": { width: "87%" },
542
- "& > [data-line]:nth-of-type(3)": { width: "66%" },
515
+ "& > [data-line]:nth-of-type(1)": { width: "[98%]" },
516
+ "& > [data-line]:nth-of-type(2)": { width: "[87%]" },
517
+ "& > [data-line]:nth-of-type(3)": { width: "[66%]" },
543
518
  });
544
- /* A reply the agent could not finish: a notice of why, with Try again under it. */
545
519
  const failure = css({
546
520
  display: "grid",
547
521
  justifyItems: "start",
548
522
  gap: "layout.contentGap",
549
- width: "100%",
523
+ width: "full",
550
524
  minWidth: "0",
551
525
  });
552
526
  const failureNotice = css({
553
527
  display: "flex",
554
528
  gap: "layout.controlGap",
555
- width: "100%",
529
+ width: "full",
556
530
  paddingBlock: "panel.default",
557
531
  paddingInline: "control.tabInline",
558
532
  borderRadius: "action",
559
- background: "token(gradients.selected)",
560
- "&::before": { background: "token(gradients.border.raised)" },
533
+ background: "[token(gradients.selected)]",
534
+ "&::before": { background: "[token(gradients.border.raised)]" },
561
535
  "& > svg": { flexShrink: "0", fontSize: "icon.small", color: "accent.red" },
562
536
  "& > [data-copy]": { display: "grid", gap: "layout.tightGap", minWidth: "0" },
563
537
  "& [data-title]": {
@@ -568,12 +542,8 @@ const failureNotice = css({
568
542
  },
569
543
  "& [data-description]": { color: "fg.secondary", fontSize: "code", lineHeight: "log" },
570
544
  });
571
- /*
572
- * The steps the agent took on its way to a reply, folded into one quiet line naming the first and
573
- * how many more; open, a row for each, its state's icon beside what it did.
574
- */
575
545
  const stepList = css({
576
- width: "100%",
546
+ width: "full",
577
547
  minWidth: "0",
578
548
  color: "fg.subtle",
579
549
  fontSize: "code",
@@ -582,17 +552,17 @@ const stepList = css({
582
552
  display: "inline-flex",
583
553
  alignItems: "center",
584
554
  gap: "control.iconGap",
585
- maxWidth: "100%",
555
+ maxWidth: "full",
586
556
  borderRadius: "control",
587
557
  cursor: "pointer",
588
558
  listStyle: "none",
589
559
  "&::-webkit-details-marker": { display: "none" },
590
560
  _hover: { color: "fg.primary" },
591
561
  _focusVisible: { focusRing: "outer" },
592
- "& > svg": { flexShrink: "0", transitionProperty: "rotate", transitionDuration: "fast" },
562
+ "& > svg": { flexShrink: "0", transitionProperty: "[rotate]", transitionDuration: "fast" },
593
563
  "& > span": { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
594
564
  },
595
- "&[open] > summary > svg": { rotate: "90deg" },
565
+ "&[open] > summary > svg": { rotate: "[90deg]" },
596
566
  "& > ol": {
597
567
  display: "grid",
598
568
  gap: "layout.controlGap",
@@ -644,7 +614,6 @@ export function ChatComposer({ value, defaultValue = "", onValueChange, onSend,
644
614
  }
645
615
  : undefined;
646
616
  const input = (_jsx(MentionInput, { ...inputProps, "aria-label": ariaLabel, autoFocus: autoFocus, className: cx(variant === "page" ? pageInput : variant === "sheet" ? sheetInput : feedInput, inputProps?.className), value: currentValue, onValueChange: setValue, triggers: mentions, mentionMark: mentionMark, listLabel: mentionsLabel, placeholder: placeholder, submitOn: variant === "feed" ? "mod+enter" : "enter", onSubmit: send }));
647
- // The sheet's send is the bar's small round one; the feed's sits in its pill, 32 round.
648
617
  const sendButton = (_jsx(IconButton, { "aria-label": submitLabel, disabled: !canSend, onClick: send, variant: variant === "page" ? "raised" : "primary", "data-send-button": "", children: _jsx(ArrowUpIcon, { className: icon({ size: "small" }), weight: "bold" }) }));
649
618
  const waiting = attachments ? _jsx("div", { "data-attachments": "", children: attachments }) : null;
650
619
  const waveform = dictation && (_jsx(ChatDictationWaveform, { levels: dictation.levels, listening: dictation.listening }));
@@ -656,34 +625,27 @@ export function ChatComposer({ value, defaultValue = "", onValueChange, onSend,
656
625
  }
657
626
  return (_jsxs("div", { ...props, className: cx(pageComposer, className), "data-ciridae-ui": "", onPasteCapture: pasteFiles, children: [waiting, input, _jsxs("div", { className: composerFooter, children: [_jsx("div", { className: actionGroup, children: tools }), waveform, _jsxs("div", { className: actionGroup, children: [actions, sendButton] })] })] }));
658
627
  }
659
- /** A tool for the sheet composer's bar, such as the paperclip: its icon alone. */
660
628
  export function ChatComposerTool({ className, ...props }) {
661
629
  return (_jsx("button", { type: "button", ...props, className: cx(composerTool, className), "data-ciridae-ui": "" }));
662
630
  }
663
- /**
664
- * Dictation's waveform: a bar per level, each as tall as its level; flat while `listening` is
665
- * off, such as while the host turns what it heard into text.
666
- */
631
+ /** Flat while `listening` is off, such as while the host turns what it heard into text. */
667
632
  export function ChatDictationWaveform({ levels, listening, className, ...props }) {
668
633
  return (_jsx("div", { ...props, className: cx(dictationWaveform, className), "data-ciridae-ui": "", "data-listening": listening || undefined, "aria-hidden": "true", children: levels.map((level, index) => (_jsx("span", { style: {
669
634
  transform: `scaleY(${listening ? Math.max(silentWaveformLevel, level) : silentWaveformLevel})`,
670
635
  } }, index))) }));
671
636
  }
672
- /** A message bubble: the user's raised on the right, another author's filled on the left. */
673
637
  export function ChatBubble({ side, groupPosition, className, ...props }) {
674
638
  return (_jsx("div", { ...props, className: cx(bubble({ side }), className), "data-ciridae-ui": "", "data-edge": side === "user" ? "gradient" : undefined, "data-group-position": groupPosition }));
675
639
  }
676
- /** A centered line between bubbles: a finished tool call, a decision, something the page did. */
677
640
  export function ChatSystemLine({ tone, className, ...props }) {
678
641
  return (_jsx("div", { ...props, className: cx(systemLine, className), "data-ciridae-ui": "", "data-tone": tone === "error" ? "error" : undefined }));
679
642
  }
680
643
  export function ChatAction({ className, ...props }) {
681
644
  return (_jsx("button", { ...props, className: cx(chatAction, className), "data-ciridae-ui": "", type: props.type ?? "button" }));
682
645
  }
683
- /** A source cited in a reply, as a card of what it says under its name. */
684
646
  export function ChatSourceCard({ label, icon, children, meta, tone = "neutral", href, className, }) {
685
647
  const content = (_jsxs(_Fragment, { children: [_jsxs("span", { "data-head": "", children: [icon, label] }), _jsx("span", { children: children }), meta && _jsx("span", { "data-meta": "", children: meta })] }));
686
- return href ? (_jsx("a", { href: href, className: cx(sourceCard, className), "data-ciridae-ui": "", "data-tone": tone, children: content })) : (_jsx("div", { className: cx(sourceCard, className), "data-ciridae-ui": "", "data-tone": tone, children: content }));
648
+ return href ? (_jsx("a", { href: href, target: "_blank", rel: "noreferrer", className: cx(sourceCard, className), "data-ciridae-ui": "", "data-tone": tone, children: content })) : (_jsx("div", { className: cx(sourceCard, className), "data-ciridae-ui": "", "data-tone": tone, children: content }));
687
649
  }
688
650
  /*
689
651
  * A conversation in time order. Close messages from one author read as a group: bubbles 4px
@@ -708,12 +670,14 @@ export function ChatThread({ entries, assistant, renderCustom, formatTime, densi
708
670
  .reverse()
709
671
  .find((entry) => entry.kind === "message" && entry.side === "user");
710
672
  const latest = useFollowLatest(rootRef, newestUserMessage?.id);
673
+ const replyBodies = useRef(new Map());
674
+ const replyAnnouncement = useFinishedReplyAnnouncement(entries, replyBodies, (entry) => authorOf(entry)?.name);
711
675
  return (_jsxs("div", { ref: rootRef, className: cx(thread, className), "data-ciridae-ui": "", "data-density": density === "compact" ? "compact" : undefined, "data-reply-style": replyStyle, children: [entries.map((entry, index) => {
712
676
  const previous = entries[index - 1];
713
677
  const newDay = dayLabels && (!previous || !isSameDay(previous.at, entry.at));
714
678
  const content = renderEntry(entry, index);
715
679
  return newDay ? (_jsxs(Fragment, { children: [_jsx(DayHeading, { at: entry.at }), content] }, entry.id)) : (content);
716
- }), _jsx("div", { className: latestSlot, children: latest.away && (_jsx(IconButton, { "aria-label": labels.chat.scrollToLatest, size: "compact", variant: "raised", onClick: latest.scrollToLatest, children: _jsx(ArrowDownIcon, { className: icon({ size: "small" }) }) })) })] }));
680
+ }), _jsx("div", { className: latestSlot, children: latest.away && (_jsx(IconButton, { "aria-label": labels.chat.scrollToLatest, size: "compact", variant: "raised", onClick: latest.scrollToLatest, children: _jsx(ArrowDownIcon, { className: icon({ size: "small" }) }) })) }), _jsx("div", { className: visuallyHidden, role: "status", "aria-live": "polite", "aria-atomic": "true", children: replyAnnouncement })] }));
717
681
  function renderEntry(entry, index) {
718
682
  if (entry.kind === "line") {
719
683
  return (_jsx(ChatSystemLine, { tone: entry.tone, children: entry.text }, entry.id));
@@ -729,18 +693,21 @@ export function ChatThread({ entries, assistant, renderCustom, formatTime, densi
729
693
  : null;
730
694
  const sourceLinks = message?.sources?.filter((source) => source.quote === undefined) ?? [];
731
695
  const sourceCards = message?.sources?.filter((source) => source.quote !== undefined) ?? [];
732
- // A plain reply drops the assistant's author and time; the user's messages keep their time.
733
696
  const footer = closesGroup &&
734
697
  entry.kind !== "typing" &&
735
698
  entry.kind !== "steps" &&
736
699
  (!plain || side === "user" || message?.sending);
737
- return (_jsxs("div", { className: entryBlock, "data-side": side, "data-avatar": avatar || undefined, "data-group-position": position, children: [tiles && _jsx(AttachmentTiles, { attachments: tiles, resolveUrl: resolveAttachmentUrl }), !(tiles && message?.body === "") && (_jsxs("div", { className: entryRow, title: message ? formatDateTime(entry.at) : undefined, children: [avatar && (_jsx("span", { className: avatarSlot, children: closesGroup && author && (_jsx(Avatar, { kind: author.kind, src: author.src, label: author.name, size: "small", "aria-hidden": "true" })) })), _jsx(ThreadEntryContent, { entry: entry, side: side, groupPosition: position, renderCustom: renderCustom, attachmentTiles: attachmentTiles, plain: plain }), message?.actions && !message.sending && (_jsx("span", { "data-entry-actions": "", children: message.actions }))] })), sourceCards.length > 0 && (_jsx("div", { className: sourceCardList, children: sourceCards.map((source) => (_jsx(ChatSourceCard, { label: source.label, icon: source.icon, meta: source.meta, tone: source.tone, href: source.href, children: source.quote }, source.id))) })), sourceLinks.length > 0 && (_jsx("span", { className: sourceRow, children: sourceLinks.map((source) => source.href ? (_jsx("a", { href: source.href, children: source.label }, source.id)) : (_jsx("span", { children: source.label }, source.id))) })), message?.suggestedActions && (_jsx("div", { className: suggestedActionRow, children: message.suggestedActions })), message?.footnote && _jsx("p", { className: footnote, children: message.footnote }), footer && (_jsx("span", { className: entryFooter, children: message?.sending ? (_jsxs(_Fragment, { children: [_jsx(Spinner, { "aria-hidden": "true", size: "compact" }), labels.chat.sending] })) : (_jsxs("span", { children: [author && (_jsxs(_Fragment, { children: [author.label ?? author.name, " · "] })), _jsx(MessageTime, { at: entry.at, format: formatTime, dated: !dayLabels })] })) }))] }, entry.id));
700
+ return (_jsxs("div", { className: entryBlock, "data-side": side, "data-avatar": avatar || undefined, "data-group-position": position, children: [tiles && _jsx(AttachmentTiles, { attachments: tiles, resolveUrl: resolveAttachmentUrl }), !(tiles && message?.body === "") && (_jsxs("div", { className: entryRow, title: message ? formatDateTime(entry.at) : undefined, children: [avatar && (_jsx("span", { className: avatarSlot, children: closesGroup && author && (_jsx(Avatar, { kind: author.kind, src: author.src, label: author.name, size: "small", "aria-hidden": "true" })) })), _jsx(ThreadEntryContent, { entry: entry, side: side, groupPosition: position, renderCustom: renderCustom, attachmentTiles: attachmentTiles, plain: plain, bodyRef: (element) => {
701
+ if (element)
702
+ replyBodies.current.set(entry.id, element);
703
+ else
704
+ replyBodies.current.delete(entry.id);
705
+ } }), message?.actions && !message.sending && (_jsx("span", { "data-entry-actions": "", children: message.actions }))] })), sourceCards.length > 0 && (_jsx("div", { className: sourceCardList, children: sourceCards.map((source) => (_jsx(ChatSourceCard, { label: source.label, icon: source.icon, meta: source.meta, tone: source.tone, href: source.href, children: source.quote }, source.id))) })), sourceLinks.length > 0 && (_jsx("span", { className: sourceRow, children: sourceLinks.map((source) => source.href ? (_jsx("a", { href: source.href, target: "_blank", rel: "noreferrer", children: source.label }, source.id)) : (_jsx("span", { children: source.label }, source.id))) })), message?.suggestedActions && (_jsx("div", { className: suggestedActionRow, children: message.suggestedActions })), message?.footnote && _jsx("p", { className: footnote, children: message.footnote }), footer && (_jsx("span", { className: entryFooter, children: message?.sending ? (_jsxs(_Fragment, { children: [_jsx(Spinner, { "aria-hidden": "true", size: "compact" }), labels.chat.sending] })) : (_jsxs("span", { children: [author && (_jsxs(_Fragment, { children: [author.label ?? author.name, " · "] })), _jsx(MessageTime, { at: entry.at, format: formatTime, dated: !dayLabels })] })) }))] }, entry.id));
738
706
  }
739
707
  }
740
- /** A day's heading in a thread that runs over days: Today, Yesterday or the weekday, then the date. */
741
708
  function DayHeading({ at }) {
742
709
  const { labels, formatDate } = useCiridaeLocale();
743
- return (_jsx(LogGroupHeader, { role: "separator", "data-day-heading": "", label: isToday(at)
710
+ return (_jsx(LogGroupHeader, { role: "separator", "data-day-heading": "", density: "panel", label: isToday(at)
744
711
  ? labels.dates.today
745
712
  : isYesterday(at)
746
713
  ? labels.dates.yesterday
@@ -768,14 +735,15 @@ function AttachmentTile({ attachment, resolveUrl, onOpen, }) {
768
735
  const content = image ? (_jsx("img", { src: url, alt: attachment.name })) : (_jsxs(_Fragment, { children: [fileTypeIcon({ name: attachment.name, type: attachment.mediaType ?? "" }), _jsx("span", { children: attachment.name })] }));
769
736
  return onOpen ? (_jsx("button", { type: "button", className: fileSquare, "data-image": image || undefined, "aria-label": labels.lightbox.preview(attachment.name), title: attachment.name, onClick: onOpen, children: content })) : (_jsx("span", { className: fileSquare, "data-image": image || undefined, title: attachment.name, children: content }));
770
737
  }
771
- function ThreadEntryContent({ entry, side, groupPosition, renderCustom, attachmentTiles: tiles, plain, }) {
738
+ function ThreadEntryContent({ entry, side, groupPosition, renderCustom, attachmentTiles: tiles, plain, bodyRef, }) {
772
739
  switch (entry.kind) {
773
740
  case "message": {
774
741
  const attachmentNames = !tiles && entry.attachments && entry.attachments.length > 0 && (_jsx("span", { className: attachmentList, children: entry.attachments.map((attachment) => attachment.url ? (_jsx("a", { href: attachment.url, target: "_blank", rel: "noreferrer", children: attachment.name }, attachment.id)) : (_jsx("span", { children: attachment.name }, attachment.id))) }));
742
+ const busy = entry.sending || entry.streaming || undefined;
775
743
  if (plain && side === "assistant") {
776
- return (_jsxs("div", { className: plainReply, "aria-busy": entry.sending || undefined, children: [entry.body, attachmentNames] }));
744
+ return (_jsxs("div", { ref: bodyRef, className: plainReply, "aria-busy": busy, children: [entry.body, attachmentNames] }));
777
745
  }
778
- return (_jsxs(ChatBubble, { side: side, groupPosition: groupPosition, "aria-busy": entry.sending || undefined, children: [entry.body, attachmentNames] }));
746
+ return (_jsxs(ChatBubble, { ref: bodyRef, side: side, groupPosition: groupPosition, "aria-busy": busy, children: [entry.body, attachmentNames] }));
779
747
  }
780
748
  case "typing":
781
749
  return _jsx(AgentThinking, { label: entry.label });
@@ -787,27 +755,77 @@ function ThreadEntryContent({ entry, side, groupPosition, renderCustom, attachme
787
755
  return renderCustom?.(entry.data);
788
756
  }
789
757
  }
790
- /** The agent at work: what it is doing beside a spinner, over lines standing in for its reply. */
791
758
  function AgentThinking({ label }) {
792
759
  const { labels } = useCiridaeLocale();
793
- return (_jsxs("div", { className: thinking, role: "status", children: [_jsxs("div", { "data-status": "", children: [_jsx(Spinner, { "aria-hidden": "true" }), _jsx("span", { children: label ?? labels.common.working })] }), _jsx("span", { "data-line": "" }), _jsx("span", { "data-line": "" }), _jsx("span", { "data-line": "" })] }));
760
+ return (_jsxs("div", { className: thinking, children: [_jsxs("div", { "data-status": "", children: [_jsx(Spinner, { "aria-hidden": "true" }), _jsx("span", { children: label ?? labels.common.working })] }), _jsx("span", { "data-line": "" }), _jsx("span", { "data-line": "" }), _jsx("span", { "data-line": "" })] }));
794
761
  }
795
- /** The steps behind a reply, folded into a line naming the first and how many more. */
796
762
  function AgentSteps({ steps }) {
763
+ const { labels } = useCiridaeLocale();
797
764
  const [first] = steps;
798
765
  if (!first)
799
766
  return null;
800
- return (_jsxs("details", { className: stepList, children: [_jsxs("summary", { children: [_jsx(CaretRightIcon, { className: cx(icon({ size: "compact" }), directional) }), _jsxs("span", { children: [first.title, steps.length > 1 && ` · +${steps.length - 1}`] })] }), _jsx("ol", { children: steps.map((step) => (_jsxs("li", { "data-status": step.status, children: [step.status === "running" ? (_jsx(Spinner, { "aria-hidden": "true" })) : step.status === "failed" ? (_jsx(WarningCircleIcon, { "aria-hidden": "true" })) : step.kind === "tool" ? (_jsx(WrenchIcon, { "aria-hidden": "true" })) : (_jsx(LightbulbIcon, { "aria-hidden": "true" })), _jsx("span", { "data-title": "", children: step.title }), step.detail && _jsx("span", { "data-detail": "", children: step.detail })] }, step.id))) })] }));
767
+ return (_jsxs("details", { className: stepList, children: [_jsxs("summary", { children: [_jsx(CaretRightIcon, { className: cx(icon({ size: "compact" }), directional) }), _jsx("span", { children: labels.chat.foldedSteps(first.title, steps.length - 1) })] }), _jsx("ol", { children: steps.map((step) => (_jsxs("li", { "data-status": step.status, children: [step.status === "running" ? (_jsx(Spinner, { "aria-hidden": "true" })) : step.status === "failed" ? (_jsx(WarningCircleIcon, { "aria-hidden": "true" })) : step.kind === "tool" ? (_jsx(WrenchIcon, { "aria-hidden": "true" })) : (_jsx(LightbulbIcon, { "aria-hidden": "true" })), _jsx("span", { "data-title": "", children: step.title }), _jsx("span", { className: visuallyHidden, children: labels.chat.stepStatus[step.status] }), step.detail && _jsx("span", { "data-detail": "", children: step.detail })] }, step.id))) })] }));
801
768
  }
802
- /** A reply that did not finish: a notice of why, and Try again under it. */
803
769
  function FailureNotice({ title, message, onRetry, }) {
804
770
  const { labels } = useCiridaeLocale();
805
771
  return (_jsxs("div", { className: failure, children: [_jsxs("div", { className: failureNotice, role: "alert", "data-ciridae-ui": "", "data-edge": "gradient", children: [_jsx(WarningCircleIcon, { "aria-hidden": "true" }), _jsxs("div", { "data-copy": "", children: [_jsx("span", { "data-title": "", children: title ?? labels.chat.couldNotAnswer }), message && _jsx("span", { "data-description": "", children: message })] })] }), _jsx(Button, { variant: "secondary", size: "medium", icon: _jsx(ArrowClockwiseIcon, { className: icon({ size: "small" }) }), onClick: onRetry, children: labels.common.tryAgain })] }));
806
772
  }
807
- /** The side an entry sits on: the agent's work, its steps and failures are the assistant's. */
808
773
  function entrySide(entry) {
809
774
  return entry.kind === "message" || entry.kind === "custom" ? entry.side : "assistant";
810
775
  }
776
+ /**
777
+ * What the thread's live region says: the newest assistant reply that has finished since the
778
+ * last render, as its author and its text. A reply finishes as it appears without `streaming`, or
779
+ * as its `streaming` turns off. Replies already there when the thread mounts, or when another
780
+ * conversation replaces the entries (its first entry changes), are never announced.
781
+ */
782
+ function useFinishedReplyAnnouncement(entries, replyBodies, authorNameOf) {
783
+ const { labels } = useCiridaeLocale();
784
+ const [announcement, setAnnouncement] = useState("");
785
+ // Every assistant reply the thread has drawn and whether it was still arriving, for the
786
+ // conversation whose first entry is `conversationKey`.
787
+ const drawnRepliesRef = useRef(null);
788
+ useEffect(() => {
789
+ const replies = entries.filter((entry) => entry.kind === "message" && entry.side === "assistant");
790
+ const conversationKey = entries[0]?.id;
791
+ const drawnReplies = drawnRepliesRef.current;
792
+ if (drawnReplies?.conversationKey !== conversationKey) {
793
+ drawnRepliesRef.current = {
794
+ conversationKey,
795
+ streamingByReplyId: new Map(replies.map((reply) => [reply.id, reply.streaming ?? false])),
796
+ };
797
+ if (drawnReplies)
798
+ setAnnouncement("");
799
+ return;
800
+ }
801
+ let newestFinishedReply;
802
+ for (const reply of replies) {
803
+ const streaming = reply.streaming ?? false;
804
+ if (!streaming && drawnReplies.streamingByReplyId.get(reply.id) !== false) {
805
+ newestFinishedReply = reply;
806
+ }
807
+ drawnReplies.streamingByReplyId.set(reply.id, streaming);
808
+ }
809
+ if (!newestFinishedReply)
810
+ return;
811
+ const text = readReplyTextToAnnounce(replyBodies.current.get(newestFinishedReply.id));
812
+ if (!text)
813
+ return;
814
+ const authorName = authorNameOf(newestFinishedReply);
815
+ setAnnouncement(authorName ? labels.chat.replyAnnouncement(authorName, text) : text);
816
+ }, [entries, replyBodies, authorNameOf, labels.chat]);
817
+ return announcement;
818
+ }
819
+ /**
820
+ * A reply's text as one line, cut at about `announcedReplyMaximumLength` characters. `innerText`,
821
+ * since `textContent` runs one paragraph's last word into the next one's first.
822
+ */
823
+ function readReplyTextToAnnounce(body) {
824
+ const text = (body?.innerText ?? "").replace(/\s+/g, " ").trim();
825
+ if (text.length <= announcedReplyMaximumLength)
826
+ return text;
827
+ return `${text.slice(0, announcedReplyMaximumLength).trimEnd()}…`;
828
+ }
811
829
  /* A message's time, which turns into its clock time while hovered. */
812
830
  const messageTime = css({
813
831
  "& > [data-exact]": { display: "none" },
@@ -893,12 +911,16 @@ function useFollowLatest(rootRef, sentMessageId) {
893
911
  scroller.scrollTop = scroller.scrollHeight;
894
912
  };
895
913
  // Content that grew since the last look keeps a reader at the foot there, whichever of its
896
- // resize or the scroll it caused is seen first; a reader's own scroll leaves the height.
914
+ // resize or the scroll it caused is seen first; a reader's own scroll leaves the height. So
915
+ // does a view that changed size, as when the composer grows a line or takes a file.
897
916
  let lastHeight = scroller.scrollHeight;
917
+ let lastViewHeight = scroller.clientHeight;
898
918
  const follow = () => {
899
919
  const grew = scroller.scrollHeight !== lastHeight;
920
+ const viewResized = scroller.clientHeight !== lastViewHeight;
900
921
  lastHeight = scroller.scrollHeight;
901
- if (grew && atFootRef.current)
922
+ lastViewHeight = scroller.clientHeight;
923
+ if ((grew || viewResized) && atFootRef.current)
902
924
  return toFoot();
903
925
  const distance = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;
904
926
  atFootRef.current = distance < followDistancePx;