@ciridae/ui 1.0.3 → 1.0.5

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 (124) hide show
  1. package/dist/chat/conversation-source.d.ts +15 -1
  2. package/dist/chat/describe-tool-call.d.ts +3 -3
  3. package/dist/chat/describe-tool-call.js +3 -3
  4. package/dist/chat/message-text.js +2 -2
  5. package/dist/ciridae.css +16 -4
  6. package/dist/components/activity-feed.d.ts +1 -0
  7. package/dist/components/activity-feed.js +1 -0
  8. package/dist/components/app-frame.d.ts +13 -1
  9. package/dist/components/app-frame.js +45 -3
  10. package/dist/components/app-navigation.d.ts +28 -1
  11. package/dist/components/app-navigation.js +110 -14
  12. package/dist/components/badge.d.ts +29 -4
  13. package/dist/components/badge.js +134 -15
  14. package/dist/components/bulk-action-bar.js +3 -3
  15. package/dist/components/button.d.ts +10 -3
  16. package/dist/components/button.js +77 -44
  17. package/dist/components/calendar.js +44 -17
  18. package/dist/components/centered-page.js +2 -2
  19. package/dist/components/chart.d.ts +1 -1
  20. package/dist/components/chart.js +16 -11
  21. package/dist/components/chat-client-source.js +47 -6
  22. package/dist/components/chat-sheet-controller.d.ts +1 -0
  23. package/dist/components/chat-sheet-controller.js +1 -0
  24. package/dist/components/chat-sheet.d.ts +10 -2
  25. package/dist/components/chat-sheet.js +74 -37
  26. package/dist/components/chat.d.ts +90 -14
  27. package/dist/components/chat.js +401 -101
  28. package/dist/components/checkbox-item.d.ts +5 -2
  29. package/dist/components/checkbox-item.js +9 -5
  30. package/dist/components/chip.d.ts +5 -0
  31. package/dist/components/chip.js +27 -1
  32. package/dist/components/column-settings.js +1 -1
  33. package/dist/components/command-menu.d.ts +5 -1
  34. package/dist/components/command-menu.js +98 -32
  35. package/dist/components/command-palette.d.ts +6 -2
  36. package/dist/components/command-palette.js +20 -10
  37. package/dist/components/data-display.d.ts +35 -4
  38. package/dist/components/data-display.js +138 -19
  39. package/dist/components/data-grid.d.ts +14 -2
  40. package/dist/components/data-grid.js +61 -26
  41. package/dist/components/date-range-picker.js +9 -20
  42. package/dist/components/drawer.d.ts +5 -0
  43. package/dist/components/drawer.js +91 -54
  44. package/dist/components/editable-field.d.ts +17 -3
  45. package/dist/components/editable-field.js +57 -27
  46. package/dist/components/email-input.js +6 -5
  47. package/dist/components/environment-frame.d.ts +1 -1
  48. package/dist/components/environment-frame.js +37 -6
  49. package/dist/components/filter-bar.js +39 -28
  50. package/dist/components/form-controls.d.ts +9 -0
  51. package/dist/components/form-controls.js +46 -22
  52. package/dist/components/form-layout.d.ts +7 -3
  53. package/dist/components/form-layout.js +68 -23
  54. package/dist/components/glass-menu.d.ts +3 -0
  55. package/dist/components/glass-menu.js +63 -0
  56. package/dist/components/gradient-edge.d.ts +9 -0
  57. package/dist/components/gradient-edge.js +31 -0
  58. package/dist/components/input.d.ts +4 -2
  59. package/dist/components/input.js +26 -15
  60. package/dist/components/keycap.d.ts +9 -5
  61. package/dist/components/keycap.js +57 -19
  62. package/dist/components/lightbox.js +2 -1
  63. package/dist/components/locale.d.ts +10 -0
  64. package/dist/components/locale.js +18 -2
  65. package/dist/components/log-entry.d.ts +40 -0
  66. package/dist/components/log-entry.js +146 -0
  67. package/dist/components/mention-input.js +4 -3
  68. package/dist/components/menu.d.ts +6 -2
  69. package/dist/components/menu.js +34 -37
  70. package/dist/components/meter.d.ts +9 -1
  71. package/dist/components/meter.js +64 -6
  72. package/dist/components/navigation.d.ts +21 -4
  73. package/dist/components/navigation.js +84 -19
  74. package/dist/components/notification-tray.js +5 -4
  75. package/dist/components/overlays.d.ts +3 -1
  76. package/dist/components/overlays.js +69 -40
  77. package/dist/components/page-frame.js +1 -1
  78. package/dist/components/panel.d.ts +1 -1
  79. package/dist/components/panel.js +18 -12
  80. package/dist/components/radio-card.js +10 -9
  81. package/dist/components/record.d.ts +25 -1
  82. package/dist/components/record.js +147 -18
  83. package/dist/components/segmented-bar.d.ts +3 -1
  84. package/dist/components/segmented-bar.js +16 -6
  85. package/dist/components/select.d.ts +5 -2
  86. package/dist/components/select.js +103 -127
  87. package/dist/components/shell-skeleton.js +1 -1
  88. package/dist/components/shell.d.ts +2 -2
  89. package/dist/components/shell.js +43 -45
  90. package/dist/components/sidebar.d.ts +7 -2
  91. package/dist/components/sidebar.js +55 -27
  92. package/dist/components/single-line-text.d.ts +2 -0
  93. package/dist/components/single-line-text.js +16 -0
  94. package/dist/components/site-banner.d.ts +5 -2
  95. package/dist/components/site-banner.js +17 -6
  96. package/dist/components/skeleton.d.ts +7 -6
  97. package/dist/components/skeleton.js +28 -16
  98. package/dist/components/skeletons.d.ts +12 -0
  99. package/dist/components/skeletons.js +71 -8
  100. package/dist/components/stages.d.ts +78 -6
  101. package/dist/components/stages.js +512 -156
  102. package/dist/components/stat-card.d.ts +10 -2
  103. package/dist/components/stat-card.js +92 -37
  104. package/dist/components/status-banner.js +1 -1
  105. package/dist/components/tabs.js +26 -13
  106. package/dist/components/theme-toggle.d.ts +4 -0
  107. package/dist/components/theme-toggle.js +67 -0
  108. package/dist/components/toast.d.ts +9 -2
  109. package/dist/components/toast.js +78 -29
  110. package/dist/preset.js +291 -33
  111. package/dist/styled-system/styles.css +3107 -1039
  112. package/dist/styled-system/tokens/index.mjs +602 -22
  113. package/dist/styled-system/tokens/tokens.d.ts +11 -11
  114. package/dist/styled-system/types/prop-type.d.ts +2 -2
  115. package/dist/styles.css +16 -4
  116. package/dist/theme/color.d.ts +3 -0
  117. package/dist/theme/color.js +5 -0
  118. package/dist/theme/derive.d.ts +9 -2
  119. package/dist/theme/derive.js +72 -12
  120. package/dist/theme/palettes.d.ts +49 -6
  121. package/dist/theme/palettes.js +84 -15
  122. package/dist/theme/tokens.d.ts +3 -3
  123. package/dist/theme/tokens.js +108 -32
  124. package/package.json +9 -1
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { ArrowClockwiseIcon, ArrowDownIcon, ArrowUpIcon } from "@phosphor-icons/react";
2
+ import { ArrowClockwiseIcon, ArrowDownIcon, ArrowUpIcon, CaretRightIcon, LightbulbIcon, WarningCircleIcon, WrenchIcon, } from "@phosphor-icons/react";
3
3
  import { isSameDay, isToday, isYesterday } from "date-fns";
4
4
  import { Fragment, useEffect, useRef, useState, useSyncExternalStore, } from "react";
5
5
  import { css, cva, cx } from "#primitives/css";
@@ -7,20 +7,20 @@ import { listMessageGroupPositions, } from "../chat/message-group-position.js";
7
7
  import { Button, IconButton } from "./button.js";
8
8
  import { Avatar } from "./data-display.js";
9
9
  import { fileSquare, fileTypeIcon } from "./dropzone.js";
10
- import { icon, Spinner } from "./icon.js";
10
+ import { directional, icon, Spinner } from "./icon.js";
11
11
  import { Lightbox } from "./lightbox.js";
12
12
  import { useCiridaeLocale } from "./locale.js";
13
+ import { LogGroupHeader } from "./log-entry.js";
13
14
  import { MentionInput } from "./mention-input.js";
14
15
  import { useResolvedFileUrl } from "./resolved-file-url.js";
15
16
  export { MessageText, splitSources, } from "../chat/message-text.js";
16
17
  /*
17
- * Two composers share one behaviour: `page` is the wide card of a chat page, and `sheet` the
18
- * composer at the foot of the agent sheet, its parts side by side as a messaging app's: the tools
19
- * as bare icons, the text in a box of its own and a round send, all level with the box's last
20
- * line. The box reads as a pill while it holds one line and as a soft card as it grows, line by
21
- * line to six before it scrolls; what goes with the message, such as files, waits as chips just
22
- * above it. `feed` is the sheet's composer for a comment under an activity feed: Enter breaks the
23
- * line there, and Cmd/Ctrl+Enter sends.
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
24
  */
25
25
  const pageComposer = css({
26
26
  display: "flex",
@@ -47,47 +47,136 @@ const pageInput = css({
47
47
  _phone: { minHeight: "chat.inputMobile", padding: "panel.comfortable" },
48
48
  });
49
49
  const sheetComposer = css({
50
+ display: "flex",
51
+ flexDirection: "column",
52
+ gap: "layout.contentGap",
53
+ width: "100%",
54
+ minWidth: "0",
55
+ padding: "panel.default",
56
+ border: "1px solid token(colors.border.control)",
57
+ borderRadius: "field",
58
+ background: "bg.field",
59
+ _focusWithin: { borderColor: "border.focus", boxShadow: "focus" },
60
+ });
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.
63
+ const sheetInput = css({
64
+ maxHeight: "6lh",
65
+ marginBlock: "calc((1em - 1lh) / 2)",
66
+ fontSize: "body",
67
+ fontWeight: "medium",
68
+ lineHeight: "body",
69
+ });
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.
72
+ const sheetBar = css({
73
+ display: "flex",
74
+ alignItems: "center",
75
+ justifyContent: "space-between",
76
+ gap: "layout.controlGap",
77
+ minWidth: "0",
78
+ "& > [data-tools], & > [data-send]": {
79
+ display: "flex",
80
+ flexShrink: "0",
81
+ alignItems: "center",
82
+ gap: "layout.controlGap",
83
+ },
84
+ "& > [data-send] > [data-send-button]": { width: "control.send", height: "control.send" },
85
+ });
86
+ const feedComposer = css({
50
87
  display: "flex",
51
88
  alignItems: "flex-end",
52
89
  gap: "layout.tightGap",
53
90
  width: "100%",
54
91
  minWidth: "0",
55
92
  });
56
- // The tools and send: as tall as the box at one line, so they stay level with its last line.
57
- const sheetSide = css({
93
+ // The tools beside the feed's pill: as tall as its send, so they stay level with its last line.
94
+ const feedTools = css({
58
95
  display: "flex",
59
96
  flexShrink: "0",
60
97
  alignItems: "center",
61
98
  gap: "layout.tightGap",
62
- height: "control.compact",
99
+ height: "calc(token(sizes.control.compact) + 2 * token(spacing.control.compactBlock))",
63
100
  });
64
- const sheetColumn = css({ display: "grid", flex: "1", minWidth: "0", gap: "layout.tightGap" });
65
- const sheetBox = css({
101
+ const feedColumn = css({ display: "grid", flex: "1", minWidth: "0", gap: "layout.tightGap" });
102
+ // The pill's 1px stroke sits inside its padding, so a one-line pill is the kit's height: the send
103
+ // plus its padding above and below.
104
+ const feedBox = css({
66
105
  display: "flex",
67
106
  alignItems: "flex-end",
107
+ gap: "layout.controlGap",
68
108
  minWidth: "0",
109
+ paddingBlock: "calc(token(spacing.control.compactBlock) - token(sizes.border.hairline))",
110
+ paddingInlineStart: "control.compactInline",
111
+ paddingInlineEnd: "control.compactBlock",
69
112
  border: "1px solid token(colors.border.control)",
70
113
  // Half the one-line height: a pill at one line, a soft card past it.
71
- borderRadius: "calc(token(sizes.control.compact) / 2)",
72
- background: "bg.field",
114
+ borderRadius: "calc((token(sizes.control.compact) + 2 * token(spacing.control.compactBlock)) / 2)",
115
+ background: "token(gradients.card)",
73
116
  _focusWithin: { borderColor: "border.focus", boxShadow: "focus" },
74
117
  "& > [data-actions]": {
75
118
  display: "flex",
76
119
  flexShrink: "0",
77
120
  alignItems: "center",
78
- height: "calc(token(sizes.control.compact) - 2px)",
79
- paddingInlineEnd: "control.trackInset",
121
+ height: "control.compact",
122
+ },
123
+ "& > [data-actions] > [data-send-button]": {
124
+ width: "control.compact",
125
+ height: "control.compact",
80
126
  },
81
127
  });
82
- // Each line keeps the one-line box's rhythm, so the first wrap only adds a line.
83
- const sheetInput = css({
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.
130
+ const feedInput = css({
84
131
  flex: "1",
85
- paddingInline: "control.compactInline",
86
- paddingBlock: "calc((token(sizes.control.compact) - 2px - 1lh) / 2)",
87
- maxHeight: "calc(5lh + token(sizes.control.compact) - 2px)",
88
- fontSize: "caption",
89
- lineHeight: "compact",
132
+ paddingBlock: "calc((token(sizes.control.compact) - 1lh) / 2)",
133
+ maxHeight: "calc(5lh + token(sizes.control.compact))",
134
+ fontSize: "body",
135
+ fontWeight: "medium",
136
+ lineHeight: "body",
137
+ });
138
+ /* A tool in the sheet composer's bar: a bare icon, with no face of its own. */
139
+ const composerTool = css({
140
+ display: "inline-flex",
141
+ padding: "none",
142
+ border: "0",
143
+ borderRadius: "control",
144
+ background: "transparent",
145
+ color: "fg.subtle",
146
+ cursor: "pointer",
147
+ _hover: { color: "fg.primary" },
148
+ _focusVisible: { focusRing: "outer" },
149
+ _disabled: { opacity: "disabled", cursor: "not-allowed" },
150
+ "&[aria-pressed=true]": { color: "fg.primary" },
90
151
  });
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
+ */
157
+ const dictationWaveform = css({
158
+ display: "flex",
159
+ flex: "1",
160
+ alignItems: "center",
161
+ justifyContent: "space-between",
162
+ gap: "control.trackInset",
163
+ minWidth: "0",
164
+ height: "control.shortcut",
165
+ overflow: "hidden",
166
+ "& > span": {
167
+ flex: "1",
168
+ minWidth: "1px",
169
+ // 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%",
172
+ borderRadius: "round",
173
+ background: "fg.subtle",
174
+ transformOrigin: "center",
175
+ },
176
+ "&[data-listening] > span": { background: "fg.secondary" },
177
+ });
178
+ /** How short a silent bar of the dictation waveform is, as a share of its full height. */
179
+ const silentWaveformLevel = 0.12;
91
180
  const composerFooter = css({
92
181
  display: "flex",
93
182
  alignItems: "center",
@@ -133,39 +222,47 @@ const chatAction = css({
133
222
  _hover: { color: "fg.primary", borderColor: "fg.muted" },
134
223
  });
135
224
  /*
136
- * A message bubble in the agent sheet: the user's on the right in the primary face, the
137
- * assistant's on the left in the neutral fill. The corner on its side is small where it joins the
138
- * bubble above or below it, so a run of messages reads as one stack.
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.
139
228
  */
140
229
  const bubble = cva({
141
230
  base: {
142
231
  position: "relative",
143
232
  maxWidth: "chat.bubble",
144
- paddingBlock: "control.compactBlock",
145
- paddingInline: "control.compactInline",
233
+ paddingBlock: "field.block",
234
+ paddingInline: "control.tabInline",
146
235
  borderRadius: "field",
147
- fontSize: "caption",
148
- lineHeight: "log",
236
+ color: "fg.primary",
237
+ fontSize: "body",
238
+ lineHeight: "compact",
149
239
  overflowWrap: "anywhere",
150
240
  },
151
241
  variants: {
152
242
  side: {
153
243
  user: {
154
244
  alignSelf: "flex-end",
155
- borderEndEndRadius: "keycap",
156
- background: "bg.primary",
157
- color: "bg.canvas",
158
- _light: { color: "fg.onAccent" },
245
+ // 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, which the edge's transparent border already counts,
247
+ // 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)",
249
+ lineHeight: "body",
250
+ background: "token(gradients.selected)",
159
251
  whiteSpace: "pre-wrap",
252
+ "&::before": { background: "token(gradients.border.selected)" },
253
+ "&[data-group-position=first], &[data-group-position=middle]": {
254
+ borderEndEndRadius: "keycap",
255
+ },
160
256
  "&[data-group-position=middle], &[data-group-position=last]": {
161
257
  borderStartEndRadius: "keycap",
162
258
  },
163
259
  },
164
260
  assistant: {
165
261
  alignSelf: "flex-start",
166
- borderEndStartRadius: "keycap",
167
262
  background: "bg.fill",
168
- color: "fg.primary",
263
+ "&[data-group-position=first], &[data-group-position=middle]": {
264
+ borderEndStartRadius: "keycap",
265
+ },
169
266
  "&[data-group-position=middle], &[data-group-position=last]": {
170
267
  borderStartStartRadius: "keycap",
171
268
  },
@@ -173,6 +270,17 @@ const bubble = cva({
173
270
  },
174
271
  },
175
272
  });
273
+ /* An answer in a thread of `plain` replies: text on the sheet itself, the thread's full width. */
274
+ const plainReply = css({
275
+ display: "grid",
276
+ gap: "layout.contentGap",
277
+ width: "100%",
278
+ minWidth: "0",
279
+ color: "fg.primary",
280
+ fontSize: "body",
281
+ lineHeight: "body",
282
+ overflowWrap: "anywhere",
283
+ });
176
284
  /** A line about the conversation rather than in it, centered between bubbles. */
177
285
  const systemLine = css({
178
286
  alignSelf: "center",
@@ -184,16 +292,18 @@ const systemLine = css({
184
292
  overflowWrap: "anywhere",
185
293
  "&[data-tone=error]": { color: "accent.red" },
186
294
  });
187
- const dayLabel = css({ marginBlock: "layout.controlGap", fontWeight: "medium" });
188
295
  /*
189
- * The thread: bubbles 4px apart, groups 12px apart, lines centered between them. Compact, as the
190
- * agent sheet draws it, keeps 8px above and below and between groups.
296
+ * 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.
191
299
  */
192
300
  const thread = css({
193
301
  display: "flex",
194
302
  flexDirection: "column",
195
303
  gap: "layout.tightGap",
196
304
  padding: "panel.default",
305
+ // Every line in the thread is set in the medium weight: bubbles, replies, cited cards and meta.
306
+ fontWeight: "medium",
197
307
  // The thread follows its newest entry itself: the browser's scroll anchoring would hold the
198
308
  // view on what was there as a reply lands and pull it off the foot.
199
309
  overflowAnchor: "none",
@@ -203,6 +313,15 @@ const thread = css({
203
313
  marginTop: "layout.tightGap",
204
314
  },
205
315
  },
316
+ "&[data-reply-style=plain]": {
317
+ padding: "panel.comfortable",
318
+ "& > [data-group-position=middle], & > [data-group-position=last]": {
319
+ marginTop: "layout.controlGap",
320
+ },
321
+ "& > :is([data-group-position=single], [data-group-position=first]):not(:first-child)": {
322
+ marginTop: "layout.contentGap",
323
+ },
324
+ },
206
325
  });
207
326
  /*
208
327
  * Under the thread, held at the foot of the scrolling area while the reader is away from the
@@ -222,7 +341,7 @@ const latestSlot = css({
222
341
  });
223
342
  /** How close to the bottom the reader must be for new content to keep the thread at its foot. */
224
343
  const followDistancePx = 96;
225
- /* A reply's sources under its bubble: small links in a row. */
344
+ /* A reply's sources under it: small links in a row, and the ones with a quote as cards. */
226
345
  const sourceRow = css({
227
346
  display: "flex",
228
347
  flexWrap: "wrap",
@@ -230,14 +349,72 @@ const sourceRow = css({
230
349
  columnGap: "layout.controlGap",
231
350
  rowGap: "control.trackInset",
232
351
  paddingInline: "layout.tightGap",
233
- color: "fg.muted",
352
+ color: "fg.subtle",
234
353
  fontSize: "code",
235
354
  lineHeight: "compact",
236
- "[data-side=assistant] > &": {
355
+ "[data-avatar] > &": {
237
356
  marginInlineStart: "calc(token(sizes.avatar.small) + token(spacing.control.iconGap))",
238
357
  },
358
+ "[data-reply-style=plain] > * > &": { paddingInline: "none" },
239
359
  "& a": { color: "fg.secondary", textDecoration: "none", _hover: { color: "fg.primary" } },
240
360
  });
361
+ const sourceCardList = css({
362
+ display: "grid",
363
+ gap: "layout.contentGap",
364
+ "[data-avatar] > &": {
365
+ marginInlineStart: "calc(token(sizes.avatar.small) + token(spacing.control.iconGap))",
366
+ },
367
+ });
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
+ const sourceCard = css({
373
+ display: "grid",
374
+ gap: "control.iconGap",
375
+ padding: "panel.default",
376
+ border: "1px solid token(colors.border.divider)",
377
+ borderRadius: "action",
378
+ background: "token(gradients.card)",
379
+ color: "fg.primary",
380
+ fontSize: "code",
381
+ fontWeight: "medium",
382
+ lineHeight: "log",
383
+ textDecoration: "none",
384
+ "&[data-tone=yellow]": { background: "bg.warningTint" },
385
+ "& > [data-head]": {
386
+ display: "flex",
387
+ alignItems: "center",
388
+ gap: "control.iconGap",
389
+ minWidth: "0",
390
+ color: "fg.subtle",
391
+ lineHeight: "tight",
392
+ "& > svg": { flexShrink: "0", fontSize: "icon.small" },
393
+ },
394
+ "&[data-tone=yellow] > [data-head]": { color: "accent.yellowIcon" },
395
+ "& > [data-meta]": { color: "fg.subtle", lineHeight: "tight" },
396
+ "&:is(a)": { _hover: { borderColor: "border.selected" }, _focusVisible: { focusRing: "outer" } },
397
+ });
398
+ /* Buttons under a reply for what it suggests doing, always shown. */
399
+ const suggestedActionRow = css({
400
+ display: "flex",
401
+ flexWrap: "wrap",
402
+ alignItems: "center",
403
+ gap: "layout.tightGap",
404
+ "[data-avatar] > &": {
405
+ marginInlineStart: "calc(token(sizes.avatar.small) + token(spacing.control.iconGap))",
406
+ },
407
+ });
408
+ /* A reply's closing line, such as what it read, quieter than the reply. */
409
+ const footnote = css({
410
+ margin: "none",
411
+ color: "fg.subtle",
412
+ fontSize: "code",
413
+ lineHeight: "log",
414
+ "[data-avatar] > &": {
415
+ marginInlineStart: "calc(token(sizes.avatar.small) + token(spacing.control.iconGap))",
416
+ },
417
+ });
241
418
  /* Files sent with a message, as tiles over its bubble: image thumbnails, or a file's icon and name. */
242
419
  const attachmentTiles = css({
243
420
  display: "flex",
@@ -245,7 +422,7 @@ const attachmentTiles = css({
245
422
  gap: "layout.tightGap",
246
423
  maxWidth: "chat.bubble",
247
424
  "[data-side=user] > &": { justifyContent: "flex-end" },
248
- "[data-side=assistant] > &": {
425
+ "[data-avatar] > &": {
249
426
  marginInlineStart: "calc(token(sizes.avatar.small) + token(spacing.control.iconGap))",
250
427
  },
251
428
  });
@@ -287,6 +464,8 @@ const entryBlock = css({
287
464
  "&[data-group-position=single], &[data-group-position=first]": {
288
465
  marginTop: "layout.controlGap",
289
466
  },
467
+ // A plain reply's parts, such as its cards and buttons under its text, sit 12px apart.
468
+ "&[data-side=assistant]": { "[data-reply-style=plain] > &": { gap: "layout.contentGap" } },
290
469
  // A message's own controls, such as Copy, show while it is hovered or focused.
291
470
  "& [data-entry-actions]": {
292
471
  display: "inline-flex",
@@ -307,6 +486,8 @@ const entryRow = css({
307
486
  width: "100%",
308
487
  minWidth: "0",
309
488
  "[data-side=user] > &": { justifyContent: "flex-end" },
489
+ // A plain reply's Copy sits at the top of its text, which may run long.
490
+ "[data-reply-style=plain] > [data-side=assistant] > &": { alignItems: "flex-start" },
310
491
  });
311
492
  const avatarSlot = css({ flexShrink: "0", width: "avatar.small" });
312
493
  const entryFooter = css({
@@ -314,11 +495,11 @@ const entryFooter = css({
314
495
  alignItems: "center",
315
496
  gap: "control.iconGap",
316
497
  paddingInline: "layout.tightGap",
317
- color: "fg.muted",
498
+ color: "fg.subtle",
318
499
  fontSize: "code",
319
500
  lineHeight: "compact",
320
501
  fontVariantNumeric: "tabular-nums",
321
- "[data-side=assistant] > &": {
502
+ "[data-avatar] > &": {
322
503
  marginInlineStart: "calc(token(sizes.avatar.small) + token(spacing.control.iconGap))",
323
504
  },
324
505
  });
@@ -331,37 +512,107 @@ const attachmentList = css({
331
512
  fontSize: "code",
332
513
  "& a": { color: "inherit" },
333
514
  });
334
- const typingBubble = css({
335
- display: "inline-flex",
336
- alignItems: "center",
337
- gap: "control.iconGap",
338
- color: "fg.muted",
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
+ */
519
+ const thinking = css({
520
+ display: "grid",
521
+ gap: "layout.contentGap",
522
+ width: "100%",
523
+ minWidth: "0",
524
+ "& > [data-status]": {
525
+ display: "flex",
526
+ alignItems: "center",
527
+ gap: "layout.controlGap",
528
+ minWidth: "0",
529
+ color: "fg.secondary",
530
+ fontSize: "code",
531
+ lineHeight: "tight",
532
+ "& > span": { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
533
+ },
534
+ "& > [data-line]": {
535
+ display: "block",
536
+ height: "calc(token(fontSizes.body) * 0.75)",
537
+ borderRadius: "round",
538
+ background: "token(gradients.skeletonFill)",
539
+ },
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%" },
543
+ });
544
+ /* A reply the agent could not finish: a notice of why, with Try again under it. */
545
+ const failure = css({
546
+ display: "grid",
547
+ justifyItems: "start",
548
+ gap: "layout.contentGap",
549
+ width: "100%",
550
+ minWidth: "0",
339
551
  });
340
- const errorBubble = css({
552
+ const failureNotice = css({
341
553
  display: "flex",
342
- flexWrap: "wrap",
343
- alignItems: "center",
344
554
  gap: "layout.controlGap",
345
- border: "1px solid token(colors.border.error)",
346
- background: "bg.errorSubtle",
555
+ width: "100%",
556
+ paddingBlock: "panel.default",
557
+ paddingInline: "control.tabInline",
558
+ borderRadius: "action",
559
+ background: "token(gradients.selected)",
560
+ "&::before": { background: "token(gradients.border.raised)" },
561
+ "& > svg": { flexShrink: "0", fontSize: "icon.small", color: "accent.red" },
562
+ "& > [data-copy]": { display: "grid", gap: "layout.tightGap", minWidth: "0" },
563
+ "& [data-title]": {
564
+ color: "accent.red",
565
+ fontSize: "body",
566
+ fontWeight: "medium",
567
+ lineHeight: "tight",
568
+ },
569
+ "& [data-description]": { color: "fg.secondary", fontSize: "code", lineHeight: "log" },
347
570
  });
348
- const typingDots = css({
349
- display: "inline-flex",
350
- alignItems: "center",
351
- gap: "control.trackInset",
352
- height: "1lh",
353
- "& > span": {
354
- width: "indicator.live",
355
- height: "indicator.live",
356
- borderRadius: "round",
357
- background: "fg.muted",
358
- animation: "livePulse 1.2s ease-in-out infinite",
359
- _motionReduce: { animation: "none" },
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
+ const stepList = css({
576
+ width: "100%",
577
+ minWidth: "0",
578
+ color: "fg.subtle",
579
+ fontSize: "code",
580
+ lineHeight: "tight",
581
+ "& > summary": {
582
+ display: "inline-flex",
583
+ alignItems: "center",
584
+ gap: "control.iconGap",
585
+ maxWidth: "100%",
586
+ borderRadius: "control",
587
+ cursor: "pointer",
588
+ listStyle: "none",
589
+ "&::-webkit-details-marker": { display: "none" },
590
+ _hover: { color: "fg.primary" },
591
+ _focusVisible: { focusRing: "outer" },
592
+ "& > svg": { flexShrink: "0", transitionProperty: "rotate", transitionDuration: "fast" },
593
+ "& > span": { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
594
+ },
595
+ "&[open] > summary > svg": { rotate: "90deg" },
596
+ "& > ol": {
597
+ display: "grid",
598
+ gap: "layout.controlGap",
599
+ margin: "none",
600
+ paddingBlockStart: "layout.contentGap",
601
+ paddingInlineStart: "none",
602
+ listStyle: "none",
603
+ },
604
+ "& li": {
605
+ display: "grid",
606
+ gridTemplateColumns: "token(sizes.mark.inline) minmax(0, 1fr)",
607
+ columnGap: "layout.controlGap",
608
+ alignItems: "start",
609
+ "& > svg": { fontSize: "icon.small" },
610
+ "&[data-status=failed] > svg": { color: "accent.red" },
611
+ "& > [data-title]": { color: "fg.secondary" },
612
+ "& > [data-detail]": { gridColumn: "2", lineHeight: "log", overflowWrap: "anywhere" },
360
613
  },
361
- "& > span:nth-child(2)": { animationDelay: "0.2s" },
362
- "& > span:nth-child(3)": { animationDelay: "0.4s" },
363
614
  });
364
- export function ChatComposer({ value, defaultValue = "", onValueChange, onSend, placeholder, ariaLabel: ownAriaLabel, tools, actions, submitLabel: ownSubmitLabel, sendDisabled = false, sendsWithoutText = false, attachments, onPasteFiles, variant = "page", autoFocus, mentions, mentionMark, mentionsLabel, inputProps, className, ...props }) {
615
+ export function ChatComposer({ value, defaultValue = "", onValueChange, onSend, placeholder, ariaLabel: ownAriaLabel, tools, actions, submitLabel: ownSubmitLabel, sendDisabled = false, sendsWithoutText = false, attachments, onPasteFiles, dictation, variant = "page", autoFocus, mentions, mentionMark, mentionsLabel, inputProps, className, ...props }) {
365
616
  const { labels } = useCiridaeLocale();
366
617
  const ariaLabel = ownAriaLabel ?? labels.chat.message;
367
618
  const submitLabel = ownSubmitLabel ?? labels.chat.sendMessage;
@@ -379,7 +630,6 @@ export function ChatComposer({ value, defaultValue = "", onValueChange, onSend,
379
630
  onSend?.(currentValue);
380
631
  setValue("");
381
632
  };
382
- const sheet = variant !== "page";
383
633
  // In the capture phase, ahead of the text field and of an enclosing `Dropzone`, which would
384
634
  // take the same files again.
385
635
  const pasteFiles = onPasteFiles
@@ -393,40 +643,59 @@ export function ChatComposer({ value, defaultValue = "", onValueChange, onSend,
393
643
  onPasteFiles(Array.from(files));
394
644
  }
395
645
  : undefined;
396
- const input = (_jsx(MentionInput, { ...inputProps, "aria-label": ariaLabel, autoFocus: autoFocus, className: cx(sheet ? sheetInput : pageInput, inputProps?.className), value: currentValue, onValueChange: setValue, triggers: mentions, mentionMark: mentionMark, listLabel: mentionsLabel, placeholder: placeholder, submitOn: variant === "feed" ? "mod+enter" : "enter", onSubmit: send }));
397
- // The sheet's send is quiet until there is something to send.
398
- const sendButton = (_jsx(IconButton, { "aria-label": submitLabel, disabled: !canSend, onClick: send, variant: sheet ? (canSend ? "primary" : "ghost") : "raised", children: _jsx(ArrowUpIcon, { className: icon({ size: "small" }), weight: "bold" }) }));
646
+ 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
+ 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" }) }));
399
649
  const waiting = attachments ? _jsx("div", { "data-attachments": "", children: attachments }) : null;
400
- if (sheet) {
401
- return (_jsxs("div", { ...props, className: cx(sheetComposer, className), "data-ciridae-ui": "", onPasteCapture: pasteFiles, children: [tools && _jsx("div", { className: sheetSide, children: tools }), _jsxs("div", { className: sheetColumn, children: [waiting, _jsxs("div", { className: sheetBox, children: [input, actions && _jsx("div", { "data-actions": "", children: actions })] })] }), _jsx("div", { className: sheetSide, children: sendButton })] }));
650
+ const waveform = dictation && (_jsx(ChatDictationWaveform, { levels: dictation.levels, listening: dictation.listening }));
651
+ if (variant === "sheet") {
652
+ return (_jsxs("div", { ...props, className: cx(sheetComposer, className), "data-ciridae-ui": "", onPasteCapture: pasteFiles, children: [waiting, input, _jsxs("div", { className: sheetBar, children: [_jsx("div", { "data-tools": "", children: tools }), waveform, _jsxs("div", { "data-send": "", children: [actions, sendButton] })] })] }));
653
+ }
654
+ if (variant === "feed") {
655
+ return (_jsxs("div", { ...props, className: cx(feedComposer, className), "data-ciridae-ui": "", onPasteCapture: pasteFiles, children: [tools && _jsx("div", { className: feedTools, children: tools }), _jsxs("div", { className: feedColumn, children: [waiting, _jsxs("div", { className: feedBox, children: [input, waveform, _jsxs("div", { "data-actions": "", children: [actions, sendButton] })] })] })] }));
402
656
  }
403
- 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 }), _jsxs("div", { className: actionGroup, children: [actions, sendButton] })] })] }));
657
+ 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
+ }
659
+ /** A tool for the sheet composer's bar, such as the paperclip: its icon alone. */
660
+ export function ChatComposerTool({ className, ...props }) {
661
+ return (_jsx("button", { type: "button", ...props, className: cx(composerTool, className), "data-ciridae-ui": "" }));
662
+ }
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
+ */
667
+ export function ChatDictationWaveform({ levels, listening, className, ...props }) {
668
+ 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
+ transform: `scaleY(${listening ? Math.max(silentWaveformLevel, level) : silentWaveformLevel})`,
670
+ } }, index))) }));
404
671
  }
405
- /** A message bubble in the agent sheet. */
672
+ /** A message bubble: the user's raised on the right, another author's filled on the left. */
406
673
  export function ChatBubble({ side, groupPosition, className, ...props }) {
407
- return (_jsx("div", { ...props, className: cx(bubble({ side }), className), "data-ciridae-ui": "", "data-group-position": groupPosition }));
674
+ return (_jsx("div", { ...props, className: cx(bubble({ side }), className), "data-ciridae-ui": "", "data-edge": side === "user" ? "gradient" : undefined, "data-group-position": groupPosition }));
408
675
  }
409
676
  /** A centered line between bubbles: a finished tool call, a decision, something the page did. */
410
677
  export function ChatSystemLine({ tone, className, ...props }) {
411
678
  return (_jsx("div", { ...props, className: cx(systemLine, className), "data-ciridae-ui": "", "data-tone": tone === "error" ? "error" : undefined }));
412
679
  }
413
- /** Three pulsing dots: the assistant is typing. */
414
- export function ChatTypingDots({ className, ...props }) {
415
- return (_jsxs("span", { ...props, className: cx(typingDots, className), "data-ciridae-ui": "", "aria-hidden": "true", children: [_jsx("span", {}), _jsx("span", {}), _jsx("span", {})] }));
416
- }
417
680
  export function ChatAction({ className, ...props }) {
418
681
  return (_jsx("button", { ...props, className: cx(chatAction, className), "data-ciridae-ui": "", type: props.type ?? "button" }));
419
682
  }
683
+ /** A source cited in a reply, as a card of what it says under its name. */
684
+ export function ChatSourceCard({ label, icon, children, meta, tone = "neutral", href, className, }) {
685
+ 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 }));
687
+ }
420
688
  /*
421
689
  * A conversation in time order. Close messages from one author read as a group: bubbles 4px
422
- * apart, groups 8px apart, the author's avatar beside the group's last bubble on the assistant's
690
+ * apart, groups 12px apart, the author's avatar beside the group's last bubble on the assistant's
423
691
  * side and the author and time once under it. The nearest scrolling ancestor follows the newest
424
692
  * entry while the reader is at its foot; scrolled up, reading back, a jump to the newest shows
425
693
  * instead. A message the user sends always brings the thread to its foot.
426
694
  */
427
- export function ChatThread({ entries, assistant, renderCustom, formatTime, density = "comfortable", attachmentTiles = false, dayLabels = false, resolveAttachmentUrl, className, }) {
695
+ export function ChatThread({ entries, assistant, renderCustom, formatTime, density = "comfortable", replyStyle = "bubble", attachmentTiles = false, dayLabels = false, resolveAttachmentUrl, className, }) {
428
696
  const rootRef = useRef(null);
429
697
  const { formatDateTime, labels } = useCiridaeLocale();
698
+ const plain = replyStyle === "plain";
430
699
  const authorOf = (entry) => entry.kind === "line" || entrySide(entry) === "user"
431
700
  ? undefined
432
701
  : ((entry.kind === "message" ? entry.author : undefined) ?? assistant);
@@ -439,11 +708,11 @@ export function ChatThread({ entries, assistant, renderCustom, formatTime, densi
439
708
  .reverse()
440
709
  .find((entry) => entry.kind === "message" && entry.side === "user");
441
710
  const latest = useFollowLatest(rootRef, newestUserMessage?.id);
442
- return (_jsxs("div", { ref: rootRef, className: cx(thread, className), "data-ciridae-ui": "", "data-density": density === "compact" ? "compact" : undefined, children: [entries.map((entry, index) => {
711
+ 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) => {
443
712
  const previous = entries[index - 1];
444
713
  const newDay = dayLabels && (!previous || !isSameDay(previous.at, entry.at));
445
714
  const content = renderEntry(entry, index);
446
- return newDay ? (_jsxs(Fragment, { children: [_jsx(DayLabel, { at: entry.at }), content] }, entry.id)) : (content);
715
+ return newDay ? (_jsxs(Fragment, { children: [_jsx(DayHeading, { at: entry.at }), content] }, entry.id)) : (content);
447
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" }) }) })) })] }));
448
717
  function renderEntry(entry, index) {
449
718
  if (entry.kind === "line") {
@@ -454,20 +723,28 @@ export function ChatThread({ entries, assistant, renderCustom, formatTime, densi
454
723
  const position = positions[index] ?? undefined;
455
724
  const closesGroup = position === "single" || position === "last";
456
725
  const message = entry.kind === "message" ? entry : null;
726
+ const avatar = !plain && side === "assistant";
457
727
  const tiles = attachmentTiles && message?.attachments && message.attachments.length > 0
458
728
  ? message.attachments
459
729
  : null;
460
- return (_jsxs("div", { className: entryBlock, "data-side": side, "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: [side === "assistant" && (_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 }), message?.actions && !message.sending && (_jsx("span", { "data-entry-actions": "", children: message.actions }))] })), message?.sources && message.sources.length > 0 && (_jsx("span", { className: sourceRow, children: message.sources.map((source) => source.href ? (_jsx("a", { href: source.href, children: source.label }, source.id)) : (_jsx("span", { children: source.label }, source.id))) })), closesGroup && entry.kind !== "typing" && (_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));
730
+ const sourceLinks = message?.sources?.filter((source) => source.quote === undefined) ?? [];
731
+ 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
+ const footer = closesGroup &&
734
+ entry.kind !== "typing" &&
735
+ entry.kind !== "steps" &&
736
+ (!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));
461
738
  }
462
739
  }
463
- /** A day's heading in a thread that runs over days: Today, Yesterday, or the date. */
464
- function DayLabel({ at }) {
740
+ /** A day's heading in a thread that runs over days: Today, Yesterday or the weekday, then the date. */
741
+ function DayHeading({ at }) {
465
742
  const { labels, formatDate } = useCiridaeLocale();
466
- return (_jsx(ChatSystemLine, { role: "separator", className: dayLabel, children: isToday(at)
743
+ return (_jsx(LogGroupHeader, { role: "separator", "data-day-heading": "", label: isToday(at)
467
744
  ? labels.dates.today
468
745
  : isYesterday(at)
469
746
  ? labels.dates.yesterday
470
- : formatDate(at, { weekday: "long", month: "short", day: "numeric" }) }));
747
+ : formatDate(at, { weekday: "long" }), meta: formatDate(at, { month: "short", day: "numeric" }) }));
471
748
  }
472
749
  /** Prompts to start a conversation from, stacked at the foot of an empty thread. */
473
750
  export function ChatSuggestions({ suggestions, onSelect, className }) {
@@ -491,20 +768,43 @@ function AttachmentTile({ attachment, resolveUrl, onOpen, }) {
491
768
  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 })] }));
492
769
  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 }));
493
770
  }
494
- function ThreadEntryContent({ entry, side, groupPosition, renderCustom, attachmentTiles: tiles, }) {
495
- const { labels } = useCiridaeLocale();
771
+ function ThreadEntryContent({ entry, side, groupPosition, renderCustom, attachmentTiles: tiles, plain, }) {
496
772
  switch (entry.kind) {
497
- case "message":
498
- return (_jsxs(ChatBubble, { side: side, groupPosition: groupPosition, "aria-busy": entry.sending || undefined, children: [entry.body, !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))) }))] }));
773
+ case "message": {
774
+ 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))) }));
775
+ if (plain && side === "assistant") {
776
+ return (_jsxs("div", { className: plainReply, "aria-busy": entry.sending || undefined, children: [entry.body, attachmentNames] }));
777
+ }
778
+ return (_jsxs(ChatBubble, { side: side, groupPosition: groupPosition, "aria-busy": entry.sending || undefined, children: [entry.body, attachmentNames] }));
779
+ }
499
780
  case "typing":
500
- return (_jsxs(ChatBubble, { side: side, groupPosition: groupPosition, className: typingBubble, role: "status", "aria-label": labels.chat.typing, children: [entry.label, _jsx(ChatTypingDots, {})] }));
781
+ return _jsx(AgentThinking, { label: entry.label });
782
+ case "steps":
783
+ return _jsx(AgentSteps, { steps: entry.steps });
501
784
  case "failure":
502
- return (_jsxs(ChatBubble, { side: side, groupPosition: groupPosition, className: errorBubble, role: "alert", children: [entry.message, _jsx(Button, { variant: "ghost", icon: _jsx(ArrowClockwiseIcon, { className: icon({ size: "small" }) }), onClick: entry.onRetry, children: labels.common.retry })] }));
785
+ return _jsx(FailureNotice, { title: entry.title, message: entry.message, onRetry: entry.onRetry });
503
786
  case "custom":
504
787
  return renderCustom?.(entry.data);
505
788
  }
506
789
  }
507
- /** The side an entry sits on: typing and failures are the assistant's. */
790
+ /** The agent at work: what it is doing beside a spinner, over lines standing in for its reply. */
791
+ function AgentThinking({ label }) {
792
+ 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": "" })] }));
794
+ }
795
+ /** The steps behind a reply, folded into a line naming the first and how many more. */
796
+ function AgentSteps({ steps }) {
797
+ const [first] = steps;
798
+ if (!first)
799
+ 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))) })] }));
801
+ }
802
+ /** A reply that did not finish: a notice of why, and Try again under it. */
803
+ function FailureNotice({ title, message, onRetry, }) {
804
+ const { labels } = useCiridaeLocale();
805
+ 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
+ }
807
+ /** The side an entry sits on: the agent's work, its steps and failures are the assistant's. */
508
808
  function entrySide(entry) {
509
809
  return entry.kind === "message" || entry.kind === "custom" ? entry.side : "assistant";
510
810
  }