@ciridae/ui 1.0.4 → 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 +70 -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 +112 -54
  25. package/dist/components/chat.d.ts +27 -56
  26. package/dist/components/chat.js +153 -132
  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 -7
  48. package/dist/components/editable-field.js +14 -27
  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 +41 -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 +820 -1297
  129. package/dist/styled-system/tokens/index.mjs +78 -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 +39 -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: {
@@ -243,14 +225,13 @@ const bubble = cva({
243
225
  user: {
244
226
  alignSelf: "flex-end",
245
227
  // Wrapped lines take the body's spacing. The kit sets one line at the type's own height
246
- // with its 1px stroke in the layout, so the padding gives back the extra leading and
247
- // a one-line bubble stays the kit's height.
248
- paddingBlock: "calc(token(spacing.field.block) + token(sizes.border.hairline) - (1lh - 1em) / 2)",
249
- paddingInline: "calc(token(spacing.control.tabInline) + token(sizes.border.hairline))",
228
+ // with its 1px stroke in the layout, which the edge's transparent border already counts,
229
+ // so the padding gives back the extra leading and a one-line bubble stays the kit's 36px.
230
+ paddingBlock: "[calc(token(spacing.field.block) - (1lh - 1em) / 2)]",
250
231
  lineHeight: "body",
251
- background: "token(gradients.selected)",
232
+ background: "[token(gradients.selected)]",
252
233
  whiteSpace: "pre-wrap",
253
- "&::before": { background: "token(gradients.border.selected)" },
234
+ "&::before": { background: "[token(gradients.border.selected)]" },
254
235
  "&[data-group-position=first], &[data-group-position=middle]": {
255
236
  borderEndEndRadius: "keycap",
256
237
  },
@@ -271,11 +252,10 @@ const bubble = cva({
271
252
  },
272
253
  },
273
254
  });
274
- /* An answer in a thread of `plain` replies: text on the sheet itself, the thread's full width. */
275
255
  const plainReply = css({
276
256
  display: "grid",
277
257
  gap: "layout.contentGap",
278
- width: "100%",
258
+ width: "full",
279
259
  minWidth: "0",
280
260
  color: "fg.primary",
281
261
  fontSize: "body",
@@ -285,7 +265,7 @@ const plainReply = css({
285
265
  /** A line about the conversation rather than in it, centered between bubbles. */
286
266
  const systemLine = css({
287
267
  alignSelf: "center",
288
- maxWidth: "85%",
268
+ maxWidth: "[85%]",
289
269
  color: "fg.muted",
290
270
  fontSize: "code",
291
271
  lineHeight: "compact",
@@ -295,15 +275,13 @@ const systemLine = css({
295
275
  });
296
276
  /*
297
277
  * The thread: bubbles 4px apart and groups 12px apart, lines centered between them. Compact
298
- * keeps 8px above and below and between groups. Plain replies sit 16px in from the sheet's
299
- * edges, an answer's parts 12px apart and one turn 16px from the next.
278
+ * keeps 8px above and below and between groups.
300
279
  */
301
280
  const thread = css({
302
281
  display: "flex",
303
282
  flexDirection: "column",
304
283
  gap: "layout.tightGap",
305
284
  padding: "panel.default",
306
- // Every line in the thread is set in the medium weight: bubbles, replies, cited cards and meta.
307
285
  fontWeight: "medium",
308
286
  // The thread follows its newest entry itself: the browser's scroll anchoring would hold the
309
287
  // view on what was there as a reply lands and pull it off the foot.
@@ -331,18 +309,21 @@ const thread = css({
331
309
  const latestSlot = css({
332
310
  position: "sticky",
333
311
  bottom: "layout.controlGap",
334
- zIndex: "1",
312
+ zIndex: "[1]",
335
313
  display: "flex",
336
314
  alignItems: "flex-end",
337
315
  justifyContent: "center",
338
316
  height: "0",
339
- marginTop: "calc(token(spacing.layout.tightGap) * -1)",
317
+ marginTop: "[calc(token(spacing.layout.tightGap) * -1)]",
340
318
  pointerEvents: "none",
341
319
  "& > *": { pointerEvents: "auto", boxShadow: "card" },
342
320
  });
343
321
  /** How close to the bottom the reader must be for new content to keep the thread at its foot. */
344
322
  const followDistancePx = 96;
345
- /* 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;
346
327
  const sourceRow = css({
347
328
  display: "flex",
348
329
  flexWrap: "wrap",
@@ -354,7 +335,7 @@ const sourceRow = css({
354
335
  fontSize: "code",
355
336
  lineHeight: "compact",
356
337
  "[data-avatar] > &": {
357
- marginInlineStart: "calc(token(sizes.avatar.small) + token(spacing.control.iconGap))",
338
+ marginInlineStart: "[calc(token(sizes.avatar.small) + token(spacing.control.iconGap))]",
358
339
  },
359
340
  "[data-reply-style=plain] > * > &": { paddingInline: "none" },
360
341
  "& a": { color: "fg.secondary", textDecoration: "none", _hover: { color: "fg.primary" } },
@@ -363,26 +344,22 @@ const sourceCardList = css({
363
344
  display: "grid",
364
345
  gap: "layout.contentGap",
365
346
  "[data-avatar] > &": {
366
- marginInlineStart: "calc(token(sizes.avatar.small) + token(spacing.control.iconGap))",
347
+ marginInlineStart: "[calc(token(sizes.avatar.small) + token(spacing.control.iconGap))]",
367
348
  },
368
349
  });
369
- /*
370
- * A source a reply cites, as a card: what it is under its icon, what it says, and when or by
371
- * whom. `yellow` marks a rule or a value the reply weighs, on the warm wash.
372
- */
373
350
  const sourceCard = css({
374
351
  display: "grid",
375
352
  gap: "control.iconGap",
376
353
  padding: "panel.default",
377
354
  border: "1px solid token(colors.border.divider)",
378
355
  borderRadius: "action",
379
- background: "token(gradients.card)",
356
+ background: "[token(gradients.card)]",
380
357
  color: "fg.primary",
381
358
  fontSize: "code",
382
359
  fontWeight: "medium",
383
360
  lineHeight: "log",
384
361
  textDecoration: "none",
385
- "&[data-tone=yellow]": { background: "bg.warningTint" },
362
+ "&[data-tone=warning]": { background: "bg.warningTint" },
386
363
  "& > [data-head]": {
387
364
  display: "flex",
388
365
  alignItems: "center",
@@ -392,28 +369,26 @@ const sourceCard = css({
392
369
  lineHeight: "tight",
393
370
  "& > svg": { flexShrink: "0", fontSize: "icon.small" },
394
371
  },
395
- "&[data-tone=yellow] > [data-head]": { color: "accent.yellowIcon" },
372
+ "&[data-tone=warning] > [data-head]": { color: "accent.yellowIcon" },
396
373
  "& > [data-meta]": { color: "fg.subtle", lineHeight: "tight" },
397
374
  "&:is(a)": { _hover: { borderColor: "border.selected" }, _focusVisible: { focusRing: "outer" } },
398
375
  });
399
- /* Buttons under a reply for what it suggests doing, always shown. */
400
376
  const suggestedActionRow = css({
401
377
  display: "flex",
402
378
  flexWrap: "wrap",
403
379
  alignItems: "center",
404
380
  gap: "layout.tightGap",
405
381
  "[data-avatar] > &": {
406
- marginInlineStart: "calc(token(sizes.avatar.small) + token(spacing.control.iconGap))",
382
+ marginInlineStart: "[calc(token(sizes.avatar.small) + token(spacing.control.iconGap))]",
407
383
  },
408
384
  });
409
- /* A reply's closing line, such as what it read, quieter than the reply. */
410
385
  const footnote = css({
411
386
  margin: "none",
412
387
  color: "fg.subtle",
413
388
  fontSize: "code",
414
389
  lineHeight: "log",
415
390
  "[data-avatar] > &": {
416
- marginInlineStart: "calc(token(sizes.avatar.small) + token(spacing.control.iconGap))",
391
+ marginInlineStart: "[calc(token(sizes.avatar.small) + token(spacing.control.iconGap))]",
417
392
  },
418
393
  });
419
394
  /* Files sent with a message, as tiles over its bubble: image thumbnails, or a file's icon and name. */
@@ -424,7 +399,7 @@ const attachmentTiles = css({
424
399
  maxWidth: "chat.bubble",
425
400
  "[data-side=user] > &": { justifyContent: "flex-end" },
426
401
  "[data-avatar] > &": {
427
- marginInlineStart: "calc(token(sizes.avatar.small) + token(spacing.control.iconGap))",
402
+ marginInlineStart: "[calc(token(sizes.avatar.small) + token(spacing.control.iconGap))]",
428
403
  },
429
404
  });
430
405
  /* Prompts to start from, stacked flush over the composer of an empty conversation. */
@@ -434,14 +409,16 @@ const suggestionList = css({
434
409
  alignItems: "flex-start",
435
410
  justifyContent: "flex-end",
436
411
  gap: "layout.tightGap",
437
- minHeight: "100%",
412
+ minHeight: "full",
438
413
  padding: "panel.default",
439
414
  });
440
415
  const suggestion = css({
441
416
  display: "inline-flex",
442
417
  alignItems: "center",
443
- maxWidth: "100%",
444
- 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",
445
422
  paddingInline: "control.compactInline",
446
423
  border: "1px solid token(colors.border.control)",
447
424
  borderRadius: "action",
@@ -465,7 +442,6 @@ const entryBlock = css({
465
442
  "&[data-group-position=single], &[data-group-position=first]": {
466
443
  marginTop: "layout.controlGap",
467
444
  },
468
- // A plain reply's parts, such as its cards and buttons under its text, sit 12px apart.
469
445
  "&[data-side=assistant]": { "[data-reply-style=plain] > &": { gap: "layout.contentGap" } },
470
446
  // A message's own controls, such as Copy, show while it is hovered or focused.
471
447
  "& [data-entry-actions]": {
@@ -473,18 +449,19 @@ const entryBlock = css({
473
449
  alignItems: "center",
474
450
  alignSelf: "center",
475
451
  flexShrink: "0",
476
- opacity: "0",
477
- transition: "opacity 120ms ease",
452
+ opacity: "[0]",
453
+ transitionProperty: "[opacity]",
454
+ transitionDuration: "fast",
478
455
  },
479
- "&:hover [data-entry-actions], &:focus-within [data-entry-actions]": { opacity: "1" },
480
- "@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]" } },
481
458
  });
482
459
  // Full width, so a bubble's maximum is a share of the thread rather than of its own text.
483
460
  const entryRow = css({
484
461
  display: "flex",
485
462
  alignItems: "flex-end",
486
463
  gap: "control.iconGap",
487
- width: "100%",
464
+ width: "full",
488
465
  minWidth: "0",
489
466
  "[data-side=user] > &": { justifyContent: "flex-end" },
490
467
  // A plain reply's Copy sits at the top of its text, which may run long.
@@ -501,7 +478,7 @@ const entryFooter = css({
501
478
  lineHeight: "compact",
502
479
  fontVariantNumeric: "tabular-nums",
503
480
  "[data-avatar] > &": {
504
- marginInlineStart: "calc(token(sizes.avatar.small) + token(spacing.control.iconGap))",
481
+ marginInlineStart: "[calc(token(sizes.avatar.small) + token(spacing.control.iconGap))]",
505
482
  },
506
483
  });
507
484
  const attachmentList = css({
@@ -509,18 +486,15 @@ const attachmentList = css({
509
486
  flexWrap: "wrap",
510
487
  gap: "layout.tightGap",
511
488
  marginBlockStart: "layout.tightGap",
512
- opacity: "0.8",
489
+ opacity: "[0.8]",
513
490
  fontSize: "code",
514
- "& a": { color: "inherit" },
491
+ "& a": { color: "[inherit]" },
515
492
  });
516
- /*
517
- * The agent at work, before its reply: what it is doing beside a spinner, over three lines of
518
- * the reply to come. The lines stand in for the reply's text, so each is that text's bar height.
519
- */
493
+ /* The lines stand in for the reply's text, so each is that text's bar height. */
520
494
  const thinking = css({
521
495
  display: "grid",
522
496
  gap: "layout.contentGap",
523
- width: "100%",
497
+ width: "full",
524
498
  minWidth: "0",
525
499
  "& > [data-status]": {
526
500
  display: "flex",
@@ -534,31 +508,30 @@ const thinking = css({
534
508
  },
535
509
  "& > [data-line]": {
536
510
  display: "block",
537
- height: "calc(token(fontSizes.body) * 0.75)",
511
+ height: "[calc(token(fontSizes.body) * 0.75)]",
538
512
  borderRadius: "round",
539
- background: "token(gradients.skeletonFill)",
513
+ background: "[token(gradients.skeletonFill)]",
540
514
  },
541
- "& > [data-line]:nth-of-type(1)": { width: "98%" },
542
- "& > [data-line]:nth-of-type(2)": { width: "87%" },
543
- "& > [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%]" },
544
518
  });
545
- /* A reply the agent could not finish: a notice of why, with Try again under it. */
546
519
  const failure = css({
547
520
  display: "grid",
548
521
  justifyItems: "start",
549
522
  gap: "layout.contentGap",
550
- width: "100%",
523
+ width: "full",
551
524
  minWidth: "0",
552
525
  });
553
526
  const failureNotice = css({
554
527
  display: "flex",
555
528
  gap: "layout.controlGap",
556
- width: "100%",
529
+ width: "full",
557
530
  paddingBlock: "panel.default",
558
531
  paddingInline: "control.tabInline",
559
532
  borderRadius: "action",
560
- background: "token(gradients.selected)",
561
- "&::before": { background: "token(gradients.border.raised)" },
533
+ background: "[token(gradients.selected)]",
534
+ "&::before": { background: "[token(gradients.border.raised)]" },
562
535
  "& > svg": { flexShrink: "0", fontSize: "icon.small", color: "accent.red" },
563
536
  "& > [data-copy]": { display: "grid", gap: "layout.tightGap", minWidth: "0" },
564
537
  "& [data-title]": {
@@ -569,12 +542,8 @@ const failureNotice = css({
569
542
  },
570
543
  "& [data-description]": { color: "fg.secondary", fontSize: "code", lineHeight: "log" },
571
544
  });
572
- /*
573
- * The steps the agent took on its way to a reply, folded into one quiet line naming the first and
574
- * how many more; open, a row for each, its state's icon beside what it did.
575
- */
576
545
  const stepList = css({
577
- width: "100%",
546
+ width: "full",
578
547
  minWidth: "0",
579
548
  color: "fg.subtle",
580
549
  fontSize: "code",
@@ -583,17 +552,17 @@ const stepList = css({
583
552
  display: "inline-flex",
584
553
  alignItems: "center",
585
554
  gap: "control.iconGap",
586
- maxWidth: "100%",
555
+ maxWidth: "full",
587
556
  borderRadius: "control",
588
557
  cursor: "pointer",
589
558
  listStyle: "none",
590
559
  "&::-webkit-details-marker": { display: "none" },
591
560
  _hover: { color: "fg.primary" },
592
561
  _focusVisible: { focusRing: "outer" },
593
- "& > svg": { flexShrink: "0", transitionProperty: "rotate", transitionDuration: "fast" },
562
+ "& > svg": { flexShrink: "0", transitionProperty: "[rotate]", transitionDuration: "fast" },
594
563
  "& > span": { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
595
564
  },
596
- "&[open] > summary > svg": { rotate: "90deg" },
565
+ "&[open] > summary > svg": { rotate: "[90deg]" },
597
566
  "& > ol": {
598
567
  display: "grid",
599
568
  gap: "layout.controlGap",
@@ -645,7 +614,6 @@ export function ChatComposer({ value, defaultValue = "", onValueChange, onSend,
645
614
  }
646
615
  : undefined;
647
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 }));
648
- // The sheet's send is the bar's small round one; the feed's sits in its pill, 32 round.
649
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" }) }));
650
618
  const waiting = attachments ? _jsx("div", { "data-attachments": "", children: attachments }) : null;
651
619
  const waveform = dictation && (_jsx(ChatDictationWaveform, { levels: dictation.levels, listening: dictation.listening }));
@@ -657,34 +625,27 @@ export function ChatComposer({ value, defaultValue = "", onValueChange, onSend,
657
625
  }
658
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] })] })] }));
659
627
  }
660
- /** A tool for the sheet composer's bar, such as the paperclip: its icon alone. */
661
628
  export function ChatComposerTool({ className, ...props }) {
662
629
  return (_jsx("button", { type: "button", ...props, className: cx(composerTool, className), "data-ciridae-ui": "" }));
663
630
  }
664
- /**
665
- * Dictation's waveform: a bar per level, each as tall as its level; flat while `listening` is
666
- * off, such as while the host turns what it heard into text.
667
- */
631
+ /** Flat while `listening` is off, such as while the host turns what it heard into text. */
668
632
  export function ChatDictationWaveform({ levels, listening, className, ...props }) {
669
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: {
670
634
  transform: `scaleY(${listening ? Math.max(silentWaveformLevel, level) : silentWaveformLevel})`,
671
635
  } }, index))) }));
672
636
  }
673
- /** A message bubble: the user's raised on the right, another author's filled on the left. */
674
637
  export function ChatBubble({ side, groupPosition, className, ...props }) {
675
638
  return (_jsx("div", { ...props, className: cx(bubble({ side }), className), "data-ciridae-ui": "", "data-edge": side === "user" ? "gradient" : undefined, "data-group-position": groupPosition }));
676
639
  }
677
- /** A centered line between bubbles: a finished tool call, a decision, something the page did. */
678
640
  export function ChatSystemLine({ tone, className, ...props }) {
679
641
  return (_jsx("div", { ...props, className: cx(systemLine, className), "data-ciridae-ui": "", "data-tone": tone === "error" ? "error" : undefined }));
680
642
  }
681
643
  export function ChatAction({ className, ...props }) {
682
644
  return (_jsx("button", { ...props, className: cx(chatAction, className), "data-ciridae-ui": "", type: props.type ?? "button" }));
683
645
  }
684
- /** A source cited in a reply, as a card of what it says under its name. */
685
646
  export function ChatSourceCard({ label, icon, children, meta, tone = "neutral", href, className, }) {
686
647
  const content = (_jsxs(_Fragment, { children: [_jsxs("span", { "data-head": "", children: [icon, label] }), _jsx("span", { children: children }), meta && _jsx("span", { "data-meta": "", children: meta })] }));
687
- 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 }));
688
649
  }
689
650
  /*
690
651
  * A conversation in time order. Close messages from one author read as a group: bubbles 4px
@@ -709,12 +670,14 @@ export function ChatThread({ entries, assistant, renderCustom, formatTime, densi
709
670
  .reverse()
710
671
  .find((entry) => entry.kind === "message" && entry.side === "user");
711
672
  const latest = useFollowLatest(rootRef, newestUserMessage?.id);
673
+ const replyBodies = useRef(new Map());
674
+ const replyAnnouncement = useFinishedReplyAnnouncement(entries, replyBodies, (entry) => authorOf(entry)?.name);
712
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) => {
713
676
  const previous = entries[index - 1];
714
677
  const newDay = dayLabels && (!previous || !isSameDay(previous.at, entry.at));
715
678
  const content = renderEntry(entry, index);
716
679
  return newDay ? (_jsxs(Fragment, { children: [_jsx(DayHeading, { at: entry.at }), content] }, entry.id)) : (content);
717
- }), _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 })] }));
718
681
  function renderEntry(entry, index) {
719
682
  if (entry.kind === "line") {
720
683
  return (_jsx(ChatSystemLine, { tone: entry.tone, children: entry.text }, entry.id));
@@ -730,18 +693,21 @@ export function ChatThread({ entries, assistant, renderCustom, formatTime, densi
730
693
  : null;
731
694
  const sourceLinks = message?.sources?.filter((source) => source.quote === undefined) ?? [];
732
695
  const sourceCards = message?.sources?.filter((source) => source.quote !== undefined) ?? [];
733
- // A plain reply drops the assistant's author and time; the user's messages keep their time.
734
696
  const footer = closesGroup &&
735
697
  entry.kind !== "typing" &&
736
698
  entry.kind !== "steps" &&
737
699
  (!plain || side === "user" || message?.sending);
738
- 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));
739
706
  }
740
707
  }
741
- /** A day's heading in a thread that runs over days: Today, Yesterday or the weekday, then the date. */
742
708
  function DayHeading({ at }) {
743
709
  const { labels, formatDate } = useCiridaeLocale();
744
- 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)
745
711
  ? labels.dates.today
746
712
  : isYesterday(at)
747
713
  ? labels.dates.yesterday
@@ -769,14 +735,15 @@ function AttachmentTile({ attachment, resolveUrl, onOpen, }) {
769
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 })] }));
770
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 }));
771
737
  }
772
- function ThreadEntryContent({ entry, side, groupPosition, renderCustom, attachmentTiles: tiles, plain, }) {
738
+ function ThreadEntryContent({ entry, side, groupPosition, renderCustom, attachmentTiles: tiles, plain, bodyRef, }) {
773
739
  switch (entry.kind) {
774
740
  case "message": {
775
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;
776
743
  if (plain && side === "assistant") {
777
- 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] }));
778
745
  }
779
- 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] }));
780
747
  }
781
748
  case "typing":
782
749
  return _jsx(AgentThinking, { label: entry.label });
@@ -788,27 +755,77 @@ function ThreadEntryContent({ entry, side, groupPosition, renderCustom, attachme
788
755
  return renderCustom?.(entry.data);
789
756
  }
790
757
  }
791
- /** The agent at work: what it is doing beside a spinner, over lines standing in for its reply. */
792
758
  function AgentThinking({ label }) {
793
759
  const { labels } = useCiridaeLocale();
794
- 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": "" })] }));
795
761
  }
796
- /** The steps behind a reply, folded into a line naming the first and how many more. */
797
762
  function AgentSteps({ steps }) {
763
+ const { labels } = useCiridaeLocale();
798
764
  const [first] = steps;
799
765
  if (!first)
800
766
  return null;
801
- 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))) })] }));
802
768
  }
803
- /** A reply that did not finish: a notice of why, and Try again under it. */
804
769
  function FailureNotice({ title, message, onRetry, }) {
805
770
  const { labels } = useCiridaeLocale();
806
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 })] }));
807
772
  }
808
- /** The side an entry sits on: the agent's work, its steps and failures are the assistant's. */
809
773
  function entrySide(entry) {
810
774
  return entry.kind === "message" || entry.kind === "custom" ? entry.side : "assistant";
811
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
+ }
812
829
  /* A message's time, which turns into its clock time while hovered. */
813
830
  const messageTime = css({
814
831
  "& > [data-exact]": { display: "none" },
@@ -894,12 +911,16 @@ function useFollowLatest(rootRef, sentMessageId) {
894
911
  scroller.scrollTop = scroller.scrollHeight;
895
912
  };
896
913
  // Content that grew since the last look keeps a reader at the foot there, whichever of its
897
- // 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.
898
916
  let lastHeight = scroller.scrollHeight;
917
+ let lastViewHeight = scroller.clientHeight;
899
918
  const follow = () => {
900
919
  const grew = scroller.scrollHeight !== lastHeight;
920
+ const viewResized = scroller.clientHeight !== lastViewHeight;
901
921
  lastHeight = scroller.scrollHeight;
902
- if (grew && atFootRef.current)
922
+ lastViewHeight = scroller.clientHeight;
923
+ if ((grew || viewResized) && atFootRef.current)
903
924
  return toFoot();
904
925
  const distance = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;
905
926
  atFootRef.current = distance < followDistancePx;