@ciridae/ui 1.0.2 → 1.0.4

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 (209) hide show
  1. package/dist/app/auth.d.ts +10 -0
  2. package/dist/app/auth.js +60 -0
  3. package/dist/app/error-boundary.d.ts +18 -0
  4. package/dist/app/error-boundary.js +45 -0
  5. package/dist/app/feedback.d.ts +12 -0
  6. package/dist/app/feedback.js +128 -0
  7. package/dist/app/index.d.ts +8 -0
  8. package/dist/app/index.js +8 -0
  9. package/dist/app/request-errors.d.ts +63 -0
  10. package/dist/app/request-errors.js +172 -0
  11. package/dist/app/sentry.d.ts +18 -0
  12. package/dist/app/sentry.js +52 -0
  13. package/dist/app/telemetry.d.ts +7 -0
  14. package/dist/app/telemetry.js +28 -0
  15. package/dist/app/trail.d.ts +9 -0
  16. package/dist/app/trail.js +18 -0
  17. package/dist/chat/chat-query-keys.d.ts +3 -3
  18. package/dist/chat/contract.d.ts +15 -15
  19. package/dist/chat/conversation-items-query-options.d.ts +114 -114
  20. package/dist/chat/conversation-source.d.ts +89 -0
  21. package/dist/chat/conversation-source.js +1 -0
  22. package/dist/chat/describe-tool-call.d.ts +3 -3
  23. package/dist/chat/describe-tool-call.js +3 -3
  24. package/dist/chat/message-text.d.ts +1 -1
  25. package/dist/chat/message-text.js +116 -48
  26. package/dist/chat/use-conversation-list.d.ts +6 -6
  27. package/dist/ciridae.css +16 -4
  28. package/dist/components/activity-feed.d.ts +3 -2
  29. package/dist/components/activity-feed.js +1 -0
  30. package/dist/components/agent-work.d.ts +3 -3
  31. package/dist/components/app-frame.d.ts +91 -0
  32. package/dist/components/app-frame.js +134 -0
  33. package/dist/components/app-navigation.d.ts +30 -3
  34. package/dist/components/app-navigation.js +105 -14
  35. package/dist/components/badge.d.ts +30 -5
  36. package/dist/components/badge.js +134 -15
  37. package/dist/components/brand-icons.d.ts +3 -3
  38. package/dist/components/bulk-action-bar.d.ts +1 -1
  39. package/dist/components/bulk-action-bar.js +3 -3
  40. package/dist/components/button.d.ts +12 -5
  41. package/dist/components/button.js +77 -44
  42. package/dist/components/calendar.d.ts +1 -1
  43. package/dist/components/calendar.js +44 -17
  44. package/dist/components/centered-page.d.ts +1 -1
  45. package/dist/components/centered-page.js +2 -2
  46. package/dist/components/chart.d.ts +22 -2
  47. package/dist/components/chart.js +251 -51
  48. package/dist/components/chat-client-source.d.ts +16 -0
  49. package/dist/components/chat-client-source.js +297 -0
  50. package/dist/components/chat-page.d.ts +9 -8
  51. package/dist/components/chat-page.js +7 -11
  52. package/dist/components/chat-sheet-controller.d.ts +11 -14
  53. package/dist/components/chat-sheet-controller.js +3 -3
  54. package/dist/components/chat-sheet.d.ts +18 -11
  55. package/dist/components/chat-sheet.js +188 -289
  56. package/dist/components/chat.d.ts +109 -19
  57. package/dist/components/chat.js +520 -124
  58. package/dist/components/checkbox-item.d.ts +6 -3
  59. package/dist/components/checkbox-item.js +9 -5
  60. package/dist/components/checkbox-list.d.ts +1 -1
  61. package/dist/components/chip.d.ts +6 -1
  62. package/dist/components/chip.js +27 -1
  63. package/dist/components/collapsible.d.ts +1 -1
  64. package/dist/components/color-theme.d.ts +1 -1
  65. package/dist/components/column-settings.d.ts +1 -1
  66. package/dist/components/column-settings.js +1 -1
  67. package/dist/components/command-menu.d.ts +5 -1
  68. package/dist/components/command-menu.js +98 -32
  69. package/dist/components/command-palette.d.ts +7 -3
  70. package/dist/components/command-palette.js +20 -11
  71. package/dist/components/copyable-id.d.ts +22 -5
  72. package/dist/components/copyable-id.js +46 -6
  73. package/dist/components/data-display.d.ts +39 -8
  74. package/dist/components/data-display.js +138 -19
  75. package/dist/components/data-grid.d.ts +23 -11
  76. package/dist/components/data-grid.js +61 -26
  77. package/dist/components/date-range-picker.d.ts +1 -1
  78. package/dist/components/date-range-picker.js +9 -20
  79. package/dist/components/date-time.d.ts +1 -1
  80. package/dist/components/dialog-height-context.d.ts +1 -1
  81. package/dist/components/document-viewer.d.ts +2 -2
  82. package/dist/components/drawer.d.ts +11 -6
  83. package/dist/components/drawer.js +91 -54
  84. package/dist/components/dropzone.d.ts +4 -4
  85. package/dist/components/editable-field.d.ts +12 -3
  86. package/dist/components/editable-field.js +57 -27
  87. package/dist/components/email-input.d.ts +1 -1
  88. package/dist/components/email-input.js +6 -5
  89. package/dist/components/environment-frame.d.ts +2 -2
  90. package/dist/components/environment-frame.js +37 -6
  91. package/dist/components/expandable-text.d.ts +1 -1
  92. package/dist/components/feedback-screenshot.d.ts +1 -1
  93. package/dist/components/feedback-sheet.d.ts +18 -3
  94. package/dist/components/feedback-sheet.js +216 -55
  95. package/dist/components/field-context.d.ts +1 -1
  96. package/dist/components/filter-bar.d.ts +3 -3
  97. package/dist/components/filter-bar.js +43 -29
  98. package/dist/components/form-controls.d.ts +12 -3
  99. package/dist/components/form-controls.js +46 -22
  100. package/dist/components/form-layout.d.ts +14 -10
  101. package/dist/components/form-layout.js +68 -23
  102. package/dist/components/glass-menu.d.ts +3 -0
  103. package/dist/components/glass-menu.js +63 -0
  104. package/dist/components/gradient-edge.d.ts +9 -0
  105. package/dist/components/gradient-edge.js +31 -0
  106. package/dist/components/hotkeys.d.ts +2 -2
  107. package/dist/components/icon.d.ts +1 -1
  108. package/dist/components/input.d.ts +7 -5
  109. package/dist/components/input.js +29 -17
  110. package/dist/components/keyboard-shortcuts.d.ts +2 -2
  111. package/dist/components/keycap.d.ts +10 -6
  112. package/dist/components/keycap.js +57 -19
  113. package/dist/components/layout.d.ts +13 -13
  114. package/dist/components/lightbox.d.ts +9 -3
  115. package/dist/components/lightbox.js +13 -9
  116. package/dist/components/link.d.ts +1 -1
  117. package/dist/components/locale.d.ts +42 -2
  118. package/dist/components/locale.js +43 -3
  119. package/dist/components/log-entry.d.ts +40 -0
  120. package/dist/components/log-entry.js +146 -0
  121. package/dist/components/log-list.d.ts +3 -3
  122. package/dist/components/mention-input.d.ts +2 -2
  123. package/dist/components/mention-input.js +4 -3
  124. package/dist/components/menu.d.ts +7 -3
  125. package/dist/components/menu.js +34 -37
  126. package/dist/components/meter.d.ts +10 -2
  127. package/dist/components/meter.js +64 -6
  128. package/dist/components/navigation.d.ts +21 -4
  129. package/dist/components/navigation.js +84 -19
  130. package/dist/components/notification-tray.d.ts +1 -1
  131. package/dist/components/notification-tray.js +5 -4
  132. package/dist/components/overlays.d.ts +6 -4
  133. package/dist/components/overlays.js +69 -40
  134. package/dist/components/page-frame.d.ts +4 -4
  135. package/dist/components/page-frame.js +1 -1
  136. package/dist/components/panel.d.ts +4 -4
  137. package/dist/components/panel.js +18 -12
  138. package/dist/components/profile-header.d.ts +1 -1
  139. package/dist/components/progress.d.ts +1 -1
  140. package/dist/components/project-avatar.d.ts +1 -1
  141. package/dist/components/radio-card.d.ts +2 -2
  142. package/dist/components/radio-card.js +10 -9
  143. package/dist/components/record.d.ts +29 -5
  144. package/dist/components/record.js +147 -18
  145. package/dist/components/resolved-file-url.d.ts +19 -0
  146. package/dist/components/resolved-file-url.js +49 -0
  147. package/dist/components/scroll-area.d.ts +1 -1
  148. package/dist/components/secret-dialog.d.ts +1 -1
  149. package/dist/components/segmented-bar.d.ts +4 -2
  150. package/dist/components/segmented-bar.js +16 -6
  151. package/dist/components/select.d.ts +9 -6
  152. package/dist/components/select.js +103 -127
  153. package/dist/components/settings.d.ts +4 -4
  154. package/dist/components/shell-skeleton.d.ts +1 -1
  155. package/dist/components/shell-skeleton.js +1 -1
  156. package/dist/components/shell.d.ts +21 -21
  157. package/dist/components/shell.js +43 -43
  158. package/dist/components/side-panel.d.ts +3 -3
  159. package/dist/components/sidebar.d.ts +13 -8
  160. package/dist/components/sidebar.js +55 -27
  161. package/dist/components/single-line-text.d.ts +2 -0
  162. package/dist/components/single-line-text.js +16 -0
  163. package/dist/components/site-banner.d.ts +5 -2
  164. package/dist/components/site-banner.js +17 -6
  165. package/dist/components/skeleton.d.ts +8 -7
  166. package/dist/components/skeleton.js +28 -16
  167. package/dist/components/skeletons.d.ts +17 -5
  168. package/dist/components/skeletons.js +71 -8
  169. package/dist/components/stages.d.ts +83 -11
  170. package/dist/components/stages.js +512 -156
  171. package/dist/components/stat-card.d.ts +12 -4
  172. package/dist/components/stat-card.js +92 -37
  173. package/dist/components/status-banner.d.ts +1 -1
  174. package/dist/components/status-banner.js +1 -1
  175. package/dist/components/status.d.ts +2 -2
  176. package/dist/components/steps.d.ts +1 -1
  177. package/dist/components/switch.d.ts +1 -1
  178. package/dist/components/tabs.d.ts +8 -8
  179. package/dist/components/tabs.js +26 -13
  180. package/dist/components/theme-customizer.d.ts +2 -2
  181. package/dist/components/theme-toggle.d.ts +4 -0
  182. package/dist/components/theme-toggle.js +67 -0
  183. package/dist/components/toast.d.ts +10 -3
  184. package/dist/components/toast.js +78 -29
  185. package/dist/components/use-new-version-available.d.ts +6 -0
  186. package/dist/components/use-new-version-available.js +29 -0
  187. package/dist/components/user-chip.d.ts +3 -3
  188. package/dist/components/wizard.d.ts +2 -2
  189. package/dist/feedback/contract.d.ts +136 -136
  190. package/dist/feedback/feedback-controller.d.ts +6 -6
  191. package/dist/feedback/generated/zod.gen.d.ts +28 -28
  192. package/dist/feedback/operator-contract.d.ts +11 -11
  193. package/dist/feedback/operator-controller.d.ts +32 -13
  194. package/dist/feedback/operator-controller.js +44 -11
  195. package/dist/preset.js +289 -33
  196. package/dist/styled-system/styles.css +3294 -1079
  197. package/dist/styled-system/tokens/index.mjs +598 -22
  198. package/dist/styled-system/tokens/tokens.d.ts +11 -11
  199. package/dist/styled-system/types/prop-type.d.ts +2 -2
  200. package/dist/styles.css +16 -4
  201. package/dist/theme/color.d.ts +3 -0
  202. package/dist/theme/color.js +5 -0
  203. package/dist/theme/derive.d.ts +9 -2
  204. package/dist/theme/derive.js +72 -12
  205. package/dist/theme/palettes.d.ts +49 -6
  206. package/dist/theme/palettes.js +84 -15
  207. package/dist/theme/tokens.d.ts +3 -3
  208. package/dist/theme/tokens.js +107 -32
  209. package/package.json +40 -3
@@ -2,32 +2,33 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  /*
3
3
  * The Ask Ciridae sheet: the agent's conversation about the page the user is on, as a chat.
4
4
  * Docked, it is an aside beside the page inside `AppShell`; as a drawer, it is the lib's sheet
5
- * over the page. The host owns the chat client, which conversation is open, the lines the
6
- * page adds to the thread and what a mention does on the page; the sheet owns the thread, the
7
- * composer, the files waiting to go with a message and the in-flight states. Files dropped
8
- * on the conversation, pasted into it or picked with the paperclip upload as they arrive.
9
- * `ChatConversation` is the same thread and composer without the sheet, such as for a full page.
5
+ * over the page. The host owns the conversation's `source` (the lib's chat client through
6
+ * `useChatClientSource`, or the app's own hooks), the lines the page adds to the thread and what
7
+ * a mention does on the page; the sheet owns the thread, the composer, the files waiting to go
8
+ * with a message and the in-flight states. Files dropped on the conversation, pasted
9
+ * into it or picked with the paperclip upload as they arrive. `ChatConversation` is the same
10
+ * thread and composer without the sheet, such as for a full page.
10
11
  *
11
12
  * `variant="agent"`, the default, is one long-running thread with the main agent: the tasks it
12
13
  * runs post cards into the thread under day labels, and a line over the thread sums up what it
13
14
  * is working on and what waits on the user. `variant="conversations"` is a chat per question.
14
15
  */
15
16
  import { ArrowsOutSimpleIcon, ArrowSquareOutIcon, PaperclipIcon, SidebarSimpleIcon, SparkleIcon, XIcon, } from "@phosphor-icons/react";
16
- import { Fragment, useRef, useState, } from "react";
17
+ import { useRef, useState, } from "react";
17
18
  import { css, cx } from "#primitives/css";
18
19
  import { parseMentions } from "../chat/contract.js";
19
- import { describeApprovalRequest } from "../chat/describe-tool-call.js";
20
20
  import { MessageText, splitSources } from "../chat/message-text.js";
21
- import { useChatConversation } from "../chat/use-chat-conversation.js";
22
21
  import { AgentTaskCard, AgentWork } from "./agent-work.js";
23
- import { Button, IconButton } from "./button.js";
24
- import { ChatComposer, ChatSuggestions, ChatThread } from "./chat.js";
22
+ import { IconButton } from "./button.js";
23
+ import { ChatComposer, ChatComposerTool, ChatSuggestions, ChatSystemLine, ChatThread, } from "./chat.js";
25
24
  import { CopyButton } from "./copyable-id.js";
26
25
  import { Drawer, DrawerBody } from "./drawer.js";
27
- import { Dropzone, DropzoneFileList } from "./dropzone.js";
28
- import { icon } from "./icon.js";
26
+ import { Dropzone, DropzoneFileList, useDropzonePicker } from "./dropzone.js";
27
+ import { icon, Spinner } from "./icon.js";
28
+ import { ShortcutHint } from "./keycap.js";
29
29
  import { heading } from "./layout.js";
30
30
  import { useCiridaeLocale } from "./locale.js";
31
+ export { useChatClientConversations, useChatClientSource, } from "./chat-client-source.js";
31
32
  /* The docked aside, drawn like the shell's main panel beside it. */
32
33
  const docked = css({
33
34
  display: "flex",
@@ -125,19 +126,20 @@ function useDockedWidth(asideRef) {
125
126
  }
126
127
  const dockedHeader = css({
127
128
  display: "flex",
129
+ flexShrink: "0",
128
130
  alignItems: "center",
129
131
  justifyContent: "space-between",
130
- gap: "layout.contentGap",
131
- paddingBlock: "panel.default",
132
+ gap: "layout.controlGap",
133
+ height: "shell.header",
132
134
  paddingInlineStart: "panel.comfortable",
133
135
  paddingInlineEnd: "panel.default",
134
- borderBottom: "1px solid token(colors.border.control)",
136
+ borderBottom: "1px solid token(colors.border.divider)",
135
137
  });
136
138
  const headerActions = css({ display: "flex", alignItems: "center", gap: "layout.controlGap" });
137
139
  const titleLine = css({
138
140
  display: "inline-flex",
139
141
  alignItems: "center",
140
- gap: "control.iconGap",
142
+ gap: "layout.controlGap",
141
143
  "& svg": { color: "fg.muted" },
142
144
  });
143
145
  const contextRow = css({
@@ -145,75 +147,32 @@ const contextRow = css({
145
147
  alignItems: "center",
146
148
  flexWrap: "wrap",
147
149
  gap: "layout.controlGap",
148
- paddingBlock: "layout.controlGap",
150
+ paddingBlock: "layout.contentGap",
149
151
  paddingInline: "panel.comfortable",
150
- borderBottom: "1px solid token(colors.border.control)",
151
- color: "fg.muted",
152
+ borderBottom: "1px solid token(colors.border.divider)",
153
+ color: "fg.subtle",
152
154
  fontSize: "code",
155
+ fontWeight: "medium",
156
+ lineHeight: "tight",
153
157
  });
158
+ // A record the agent reads: the raised face under its gradient edge.
154
159
  const contextChip = css({
155
160
  display: "inline-flex",
156
161
  alignItems: "center",
157
162
  gap: "control.iconGap",
158
163
  height: "control.shortcut",
159
- paddingInlineStart: "control.block",
160
- paddingInlineEnd: "control.iconInline",
161
- border: "1px solid token(colors.border.control)",
162
- borderRadius: "pill",
163
- background: "bg.raised",
164
+ // The kit counts the 1px edge into the padding.
165
+ paddingInlineStart: "calc(token(spacing.control.block) + token(sizes.border.hairline))",
166
+ paddingInlineEnd: "calc(token(spacing.control.iconInline) + token(sizes.border.hairline))",
167
+ borderRadius: "field",
168
+ background: "token(gradients.raised)",
164
169
  color: "fg.primary",
170
+ "&::before": { background: "token(gradients.border.raised)" },
165
171
  "& svg": { color: "fg.muted" },
166
172
  });
167
173
  /* The thread's scrolling area, between the header and the composer. */
168
174
  const threadScroll = css({ flex: "1", minHeight: "0", padding: "none", overflowY: "auto" });
169
- /* A card in the assistant's flow: an approval or a question, with its answers as flat rows. */
170
- const card = css({
171
- display: "grid",
172
- gap: "control.iconGap",
173
- width: "100%",
174
- maxWidth: "chat.bubble",
175
- paddingBlock: "control.block",
176
- paddingInline: "control.compactInline",
177
- border: "1px solid token(colors.border.control)",
178
- borderRadius: "field",
179
- background: "bg.card",
180
- fontSize: "caption",
181
- lineHeight: "log",
182
- });
183
- const cardLabel = css({ color: "fg.muted", fontSize: "code", lineHeight: "compact" });
184
- const argumentList = css({
185
- display: "grid",
186
- gridTemplateColumns: "auto minmax(0, 1fr)",
187
- columnGap: "layout.controlGap",
188
- margin: "none",
189
- fontSize: "code",
190
- lineHeight: "compact",
191
- "& > dt": { color: "fg.muted", whiteSpace: "nowrap" },
192
- "& > dd": { margin: "none", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
193
- });
194
- const cardActions = css({ display: "flex", justifyContent: "flex-end", gap: "layout.controlGap" });
195
- // Option rows run to the card's edges, split by its hairlines.
196
- const optionList = css({
197
- marginInline: "calc(token(spacing.control.compactInline) * -1)",
198
- marginBlockEnd: "calc(token(spacing.control.block) * -1)",
199
- "& > button": {
200
- display: "block",
201
- width: "100%",
202
- paddingBlock: "control.compactBlock",
203
- paddingInline: "control.compactInline",
204
- border: "0",
205
- borderTop: "1px solid token(colors.border.control)",
206
- background: "transparent",
207
- color: "fg.primary",
208
- font: "inherit",
209
- textAlign: "start",
210
- cursor: "pointer",
211
- _hover: { background: "bg.hover" },
212
- _focusVisible: { focusRing: "inset" },
213
- "&:last-child": { borderBottomLeftRadius: "field", borderBottomRightRadius: "field" },
214
- },
215
- });
216
- const optionDetail = css({ display: "block", color: "fg.muted", fontSize: "code" });
175
+ const centered = css({ display: "grid", placeItems: "center", height: "100%" });
217
176
  /* The thread and composer, the drop target for files; it fills what the sheet or page gives it. */
218
177
  const conversationRoot = css({
219
178
  display: "flex",
@@ -225,44 +184,44 @@ const composerFooter = css({
225
184
  display: "grid",
226
185
  gap: "layout.tightGap",
227
186
  flexShrink: "0",
228
- paddingBlock: "layout.controlGap",
229
- paddingInline: "panel.default",
230
- borderTop: "1px solid token(colors.border.control)",
231
- "&[data-layout=wide]": { borderTop: "0", paddingBlockEnd: "panel.default" },
187
+ padding: "panel.comfortable",
188
+ borderTop: "1px solid token(colors.border.divider)",
189
+ "&[data-layout=wide]": { borderTop: "0" },
232
190
  });
191
+ /* Over the composer, what goes with it: an error. */
192
+ const footerStack = css({ display: "grid", gap: "layout.tightGap", minWidth: "0" });
233
193
  /* Full page: the thread and composer in one readable column down the middle. */
234
194
  const wideColumn = css({ width: "100%", maxWidth: "chat.thread", marginInline: "auto" });
235
- const hiddenInput = css({ display: "none" });
236
195
  /* In a drawer, the context row and the conversation stack under its header. */
237
196
  const drawerStack = css({ display: "flex", flex: "1", minHeight: "0", flexDirection: "column" });
238
- /** How many of an approval's arguments show before the rest become "+N". */
239
- const maximumArgumentRows = 3;
240
- export function ChatSheet({ open, onOpenChange, title, context, placement = "drawer", onPlacementChange, onOpenFullPage, ...conversationProps }) {
197
+ export function ChatSheet({ open, onOpenChange, title, context, placement = "drawer", onPlacementChange, onOpenFullPage, shortcut, ...conversationProps }) {
241
198
  const { variant = "agent", tasks } = conversationProps;
242
199
  const { labels } = useCiridaeLocale();
243
200
  const asideRef = useRef(null);
244
201
  const dockedWidth = useDockedWidth(asideRef);
245
202
  const titleContent = (_jsxs("span", { className: titleLine, children: [_jsx(SparkleIcon, { className: icon({ size: "small" }), weight: "fill" }), title] }));
246
- const actions = (_jsxs(_Fragment, { children: [onOpenFullPage && (_jsx(IconButton, { "aria-label": labels.chat.openFullPage, variant: "ghost", size: "small", onClick: onOpenFullPage, children: _jsx(ArrowsOutSimpleIcon, { className: icon({ size: "small" }) }) })), onPlacementChange &&
203
+ const actions = (_jsxs(_Fragment, { children: [shortcut && _jsx(ShortcutHint, { hotkey: shortcut, boxed: true }), onOpenFullPage && (_jsx(IconButton, { "aria-label": labels.chat.openFullPage, variant: "ghost", size: "small", onClick: onOpenFullPage, children: _jsx(ArrowsOutSimpleIcon, { className: icon({ size: "small" }) }) })), onPlacementChange &&
247
204
  (placement === "docked" ? (_jsx(IconButton, { "aria-label": labels.chat.undock, variant: "ghost", size: "small", onClick: () => onPlacementChange("drawer"), children: _jsx(ArrowSquareOutIcon, { className: icon({ size: "small" }) }) })) : (_jsx(IconButton, { "aria-label": labels.chat.dock, variant: "ghost", size: "small", onClick: () => onPlacementChange("docked"), children: _jsx(SidebarSimpleIcon, { className: icon({ size: "small" }), mirrored: true }) })))] }));
248
- const contextContent = (_jsxs(_Fragment, { children: [context && context.length > 0 && (_jsxs("div", { className: contextRow, children: ["Reading", context.map((item) => (_jsxs("span", { className: contextChip, "data-ciridae-ui": "", children: [item.icon, item.label] }, item.label)))] })), variant === "agent" && tasks && _jsx(AgentWork, { layout: "summary", tasks: tasks })] }));
205
+ const contextContent = (_jsxs(_Fragment, { children: [context && context.length > 0 && (_jsxs("div", { className: contextRow, children: [labels.chat.reading, context.map((item) => (_jsxs("span", { className: contextChip, "data-ciridae-ui": "", "data-edge": "gradient", children: [item.icon, item.label] }, item.label)))] })), variant === "agent" && tasks && _jsx(AgentWork, { layout: "summary", tasks: tasks })] }));
249
206
  if (placement === "docked") {
250
207
  if (!open)
251
208
  return null;
252
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: resizeHandle, ...dockedWidth.handleProps }), _jsxs("aside", { ref: asideRef, className: docked, "data-ciridae-ui": "", "aria-label": typeof title === "string" ? title : undefined, style: dockedWidth.width === null ? undefined : { width: dockedWidth.width }, children: [_jsxs("header", { className: dockedHeader, children: [_jsx("h2", { className: cx(heading(), css({ lineHeight: "tight" })), children: titleContent }), _jsxs("div", { className: headerActions, children: [actions, _jsx(IconButton, { "aria-label": labels.common.close, variant: "ghost", size: "small", onClick: () => onOpenChange(false), children: _jsx(XIcon, { className: icon({ size: "small" }) }) })] })] }), contextContent, _jsx(ChatConversation, { ...conversationProps })] })] }));
209
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: resizeHandle, ...dockedWidth.handleProps }), _jsxs("aside", { ref: asideRef, className: docked, "data-ciridae-ui": "", "aria-label": typeof title === "string" ? title : undefined, style: dockedWidth.width === null ? undefined : { width: dockedWidth.width }, children: [_jsxs("header", { className: dockedHeader, children: [_jsx("h2", { className: cx(heading({ size: "small" }), css({ lineHeight: "tight" })), children: titleContent }), _jsxs("div", { className: headerActions, children: [actions, _jsx(IconButton, { "aria-label": labels.common.close, onClick: () => onOpenChange(false), children: _jsx(XIcon, { className: icon({ size: "small" }) }) })] })] }), contextContent, _jsx(ChatConversation, { ...conversationProps })] })] }));
253
210
  }
254
211
  return (_jsx(Drawer, { open: open, onOpenChange: onOpenChange, size: "narrow", title: title, icon: _jsx(SparkleIcon, { className: icon({ size: "small" }), weight: "fill" }), actions: actions, children: _jsxs("div", { className: drawerStack, children: [contextContent, _jsx(ChatConversation, { ...conversationProps, scrollBody: DrawerBody })] }) }));
255
212
  }
256
213
  /* The files for the next message: each uploads as it is added; send waits until all have. */
257
- function useComposerFiles(client) {
214
+ function useComposerFiles(upload, onUploadError) {
258
215
  const [entries, setEntries] = useState([]);
259
216
  const remove = (file) => setEntries((existing) => existing.filter((entry) => entry.file !== file));
260
217
  const add = (files) => {
218
+ if (!upload)
219
+ return;
261
220
  setEntries((existing) => [...existing, ...files.map((file) => ({ file, attachment: null }))]);
262
221
  for (const file of files) {
263
- void client.uploadAttachment(file).then((attachment) => setEntries((existing) => existing.map((entry) => (entry.file === file ? { file, attachment } : entry))), (error) => {
222
+ upload(file).then((attachment) => setEntries((existing) => existing.map((entry) => (entry.file === file ? { file, attachment } : entry))), (error) => {
264
223
  remove(file);
265
- throw error;
224
+ onUploadError(error);
266
225
  });
267
226
  }
268
227
  };
@@ -279,54 +238,53 @@ function useComposerFiles(client) {
279
238
  * The thread and the composer: everything that reads or writes the conversation. `layout="wide"`
280
239
  * sets both in one readable column for a full page; the sheet uses `sheet`.
281
240
  */
282
- export function ChatConversation({ client, conversationId, onConversationCreated, placeholder, draft, onDraftChange, composerRef, mentions, mentionMark, onMentionSelect, mentionHref, events = [], assistantName: ownAssistantName, suggestions, copyReplies = false, variant = "agent", tasks, onTaskReply, layout = "sheet", scrollBody: ScrollBody = "div", }) {
241
+ export function ChatConversation({ source, placeholder, draft, onDraftChange, composerRef, mentions, mentionMark, onMentionSelect, mentionHref, events = [], assistantName: ownAssistantName, suggestions, copyReplies = false, composerTools, dictation, variant = "agent", tasks, onTaskReply, layout = "sheet", scrollBody: ScrollBody = "div", }) {
283
242
  const { labels } = useCiridaeLocale();
284
243
  const assistantName = ownAssistantName ?? labels.chat.assistantName;
285
- const state = useChatConversation({
286
- client,
287
- conversationId,
288
- onConversationCreated,
289
- });
290
- const files = useComposerFiles(client);
291
- const [sendError, setSendError] = useState(null);
292
- const fileInputRef = useRef(null);
293
- const send = async (content, sentAttachments) => {
294
- setSendError(null);
244
+ const [ownDraft, setOwnDraft] = useState("");
245
+ const currentDraft = draft ?? ownDraft;
246
+ const setDraft = (next) => {
247
+ setOwnDraft(next);
248
+ onDraftChange?.(next);
249
+ };
250
+ const [sending, setSending] = useState(false);
251
+ const [composerError, setComposerError] = useState(null);
252
+ const showError = (error) => setComposerError(error instanceof Error ? error.message : String(error));
253
+ const files = useComposerFiles(source.upload, showError);
254
+ const send = async (text) => {
255
+ setSending(true);
256
+ setComposerError(null);
295
257
  try {
296
- await state.sendMessage(content, sentAttachments);
258
+ await source.send({ text, attachments: files.attachments });
259
+ files.clear();
297
260
  }
298
261
  catch (error) {
299
- setSendError(error instanceof Error ? error : new Error(String(error)));
262
+ // The composer cleared as it sent: the text comes back, with the files still waiting.
263
+ setDraft(text);
264
+ showError(error);
265
+ }
266
+ finally {
267
+ setSending(false);
300
268
  }
301
269
  };
302
- const renderReply = (content, plain) => (_jsx(MessageText, { content: content, plain: plain, onMentionSelect: onMentionSelect, mentionHref: mentionHref }));
303
270
  const entries = buildThread({
304
271
  labels: labels.chat,
305
- items: state.items,
306
- pendingMessages: state.pendingMessages,
307
- response: state.response,
308
- isResponseActive: state.isResponseActive,
272
+ source,
309
273
  events,
310
274
  tasks: variant === "agent" ? (tasks ?? []) : [],
311
- sendError,
312
- renderReply,
275
+ renderReply: (content, plain) => (_jsx(MessageText, { content: content, plain: plain, onMentionSelect: onMentionSelect, mentionHref: mentionHref })),
313
276
  replyActions: copyReplies ? (content) => _jsx(CopyReplyButton, { content: content }) : undefined,
314
- attachmentUrl: (attachment) => client.getAttachmentUrl(attachment),
315
- retryPending: (message) => {
316
- state.discardPendingMessage(message.localId);
317
- void send(message.content, message.attachments);
318
- },
319
- retryMessage: (message) => void send(message.content, message.attachments),
277
+ taskCard: (task) => _jsx(AgentTaskCard, { task: task, onReply: onTaskReply }),
320
278
  });
321
279
  const wide = layout === "wide";
322
- const showSuggestions = entries.length === 0 && !state.isLoading && suggestions !== undefined && suggestions.length > 0;
323
- return (_jsxs(Dropzone, { onFiles: files.add, className: conversationRoot, children: [_jsx(ScrollBody, { className: threadScroll, children: showSuggestions ? (_jsx(ChatSuggestions, { className: cx(wide && wideColumn), suggestions: suggestions, onSelect: (suggestion) => void send(suggestion, []) })) : (_jsx(ChatThread, { className: cx(wide && wideColumn), entries: entries, density: "compact", attachmentTiles: true, dayLabels: true, assistant: { name: assistantName, kind: "agent" }, renderCustom: (card) => card.kind === "approval" ? (_jsx(ApprovalCard, { item: card.item, onDecide: state.decideApproval })) : card.kind === "question" ? (_jsx(QuestionCard, { item: card.item, onAnswer: state.answerQuestion })) : (_jsx(AgentTaskCard, { task: card.task, onReply: onTaskReply })) })) }), _jsx("div", { className: composerFooter, "data-layout": layout, children: _jsx("div", { className: cx(wide && wideColumn), children: _jsx(ChatComposer, { variant: "sheet", placeholder: placeholder, autoFocus: true, value: draft, onValueChange: onDraftChange, inputProps: { ref: composerRef }, mentions: mentions, mentionMark: mentionMark, sendDisabled: files.uploading, sendsWithoutText: files.attachments.length > 0, attachments: files.files.length > 0 && (_jsx(DropzoneFileList, { variant: "tiles", files: files.files, pending: files.uploading, onRemove: files.remove })), onSend: (content) => {
324
- void send(content, files.attachments);
325
- files.clear();
326
- }, tools: _jsxs(_Fragment, { children: [_jsx(IconButton, { "aria-label": labels.chat.attachFile, size: "compact", variant: "ghost", onClick: () => fileInputRef.current?.click(), children: _jsx(PaperclipIcon, { className: icon({ size: "small" }) }) }), _jsx("input", { ref: fileInputRef, type: "file", multiple: true, className: hiddenInput, onChange: (event) => {
327
- files.add(Array.from(event.target.files ?? []));
328
- event.target.value = "";
329
- } })] }) }) }) })] }));
280
+ const showSuggestions = entries.length === 0 && !source.loading && suggestions !== undefined && suggestions.length > 0;
281
+ return (_jsxs(Dropzone, { onFiles: files.add, disabled: !source.upload, className: conversationRoot, children: [_jsx(ScrollBody, { className: threadScroll, children: source.loading && entries.length === 0 ? (_jsx("div", { className: centered, children: _jsx(Spinner, { label: labels.common.loading }) })) : showSuggestions ? (_jsx(ChatSuggestions, { className: cx(wide && wideColumn), suggestions: suggestions, onSelect: (suggestion) => void send(suggestion) })) : (_jsx(ChatThread, { className: cx(wide && wideColumn), entries: entries, replyStyle: "plain", attachmentTiles: true, dayLabels: true, resolveAttachmentUrl: source.resolveAttachmentUrl, assistant: { name: assistantName, kind: "agent" }, renderCustom: (content) => content })) }), _jsx("div", { className: composerFooter, "data-layout": layout, children: _jsxs("div", { className: cx(footerStack, wide && wideColumn), children: [composerError && (_jsx(ChatSystemLine, { tone: "error", role: "alert", children: composerError })), _jsx(ChatComposer, { variant: "sheet", placeholder: placeholder, autoFocus: true, value: currentDraft, onValueChange: setDraft, inputProps: { ref: composerRef }, mentions: mentions, mentionMark: mentionMark, sendDisabled: files.uploading || sending, sendsWithoutText: files.attachments.length > 0, onPasteFiles: source.upload ? files.add : undefined, attachments: files.files.length > 0 && (_jsx(DropzoneFileList, { variant: "tiles", files: files.files, pending: files.uploading, onRemove: files.remove })), onSend: (content) => void send(content), tools: _jsxs(_Fragment, { children: [source.upload && _jsx(AttachFilesButton, {}), composerTools] }), dictation: dictation })] }) })] }));
282
+ }
283
+ /** The paperclip: the enclosing `Dropzone`'s file picker. */
284
+ function AttachFilesButton() {
285
+ const { labels } = useCiridaeLocale();
286
+ const picker = useDropzonePicker();
287
+ return (_jsx(ChatComposerTool, { "aria-label": labels.chat.attachFile, disabled: picker.disabled, onClick: picker.openPicker, children: _jsx(PaperclipIcon, { className: icon({ size: "small" }) }) }));
330
288
  }
331
289
  /** Copies a reply's text, its mentions as their labels, and shows a tick for a moment. */
332
290
  function CopyReplyButton({ content }) {
@@ -334,88 +292,37 @@ function CopyReplyButton({ content }) {
334
292
  .map((part) => (typeof part === "string" ? part : part.label))
335
293
  .join("") }));
336
294
  }
337
- /** The conversation's items, pending messages, page events and running state in one timeline. */
338
- function buildThread({ items, pendingMessages, response, isResponseActive, events, tasks, sendError, renderReply, replyActions, attachmentUrl, retryPending, retryMessage, labels, }) {
295
+ /**
296
+ * The source's items, the page's events, the agent's tasks and how the latest turn stands, in one
297
+ * timeline: each run of the agent's steps folded into one entry, and at the latest turn's foot its
298
+ * thinking, failure or stop.
299
+ */
300
+ function buildThread({ labels, source: { items, status, failureMessage, retry }, events, tasks, renderReply, replyActions, taskCard, }) {
339
301
  const entries = [];
340
- const toAttachments = (attachments) => attachments.map((attachment) => ({
341
- id: attachment.id,
342
- name: attachment.name,
343
- mediaType: attachment.media_type,
344
- url: attachmentUrl(attachment),
345
- }));
346
- for (const item of items) {
347
- const at = Date.parse(item.created_at);
348
- const waitingOnUser = response?.id === item.response_id && response.status === "requires_action";
349
- // A reply is done once its turn is: not still running, and not failed.
350
- const turnUnfinished = response?.id === item.response_id && (isResponseActive || response.status === "failed");
351
- switch (item.type) {
352
- case "message": {
353
- const { content } = item;
354
- const { sources } = splitSources(content);
355
- entries.push({
356
- kind: "message",
357
- id: item.id,
358
- side: item.role,
359
- at,
360
- // Files sent alone keep an empty body, so the thread draws their tiles with no bubble.
361
- body: content.trim() ? renderReply(content, item.role === "user") : "",
362
- attachments: toAttachments(item.attachments),
363
- sources: item.role === "assistant"
364
- ? sources.map((source) => ({
365
- id: source.id,
366
- label: source.label,
367
- href: source.href,
368
- }))
369
- : undefined,
370
- actions: item.role === "assistant" && !turnUnfinished ? replyActions?.(content) : undefined,
371
- });
372
- break;
373
- }
374
- case "approval_request":
375
- if (item.decision === null && waitingOnUser) {
376
- entries.push({
377
- kind: "custom",
378
- id: item.id,
379
- side: "assistant",
380
- at,
381
- data: { kind: "approval", item },
382
- });
383
- }
384
- else if (item.decision !== null) {
385
- entries.push({
386
- kind: "line",
387
- id: item.id,
388
- at,
389
- text: `${describeApprovalRequest(item.tool)} · ${item.decision === "allow" ? "allowed" : "denied"}`,
390
- });
391
- }
392
- break;
393
- case "question":
394
- if (item.answers === null && waitingOnUser) {
395
- entries.push({
396
- kind: "custom",
397
- id: item.id,
398
- side: "assistant",
399
- at,
400
- data: { kind: "question", item },
401
- });
402
- }
403
- else if (item.answers !== null) {
404
- entries.push({
405
- kind: "line",
406
- id: item.id,
407
- at,
408
- text: labels.answered(item.answers.flat().filter(Boolean).join(", ")),
409
- });
410
- }
411
- break;
412
- // What the agent runs on its way to a reply stays behind its typing.
413
- case "function_call":
414
- case "function_call_output":
415
- case "reasoning":
416
- break;
302
+ // The latest turn is everything after the user's last message; its replies are done once it is.
303
+ let latestTurnStart = -1;
304
+ items.forEach((item, index) => {
305
+ if (item.kind === "message" && item.role === "user")
306
+ latestTurnStart = index;
307
+ });
308
+ const latestTurnUnfinished = status === "running" || status === "failed";
309
+ // The run of steps the items end on, which the running turn draws as what the agent is doing.
310
+ let stepRun = null;
311
+ for (const [index, item] of items.entries()) {
312
+ if (item.kind === "step") {
313
+ stepRun ?? (stepRun = { id: `steps-${item.id}`, at: item.at, steps: [] });
314
+ stepRun.steps.push({ ...item.step, id: item.id });
315
+ continue;
417
316
  }
317
+ if (stepRun)
318
+ entries.push({ kind: "steps", ...stepRun });
319
+ stepRun = null;
320
+ const inUnfinishedTurn = latestTurnUnfinished && index > latestTurnStart;
321
+ entries.push(...toEntries(item, inUnfinishedTurn));
418
322
  }
323
+ const runningSteps = status === "running" ? stepRun : null;
324
+ if (stepRun && !runningSteps)
325
+ entries.push({ kind: "steps", ...stepRun });
419
326
  for (const event of events) {
420
327
  entries.push({
421
328
  kind: "line",
@@ -432,7 +339,7 @@ function buildThread({ items, pendingMessages, response, isResponseActive, event
432
339
  id: `task-${task.id}`,
433
340
  side: "assistant",
434
341
  at: Date.parse(task.ask.askedAt),
435
- data: { kind: "task", task },
342
+ data: taskCard(task),
436
343
  });
437
344
  }
438
345
  if ((task.status === "done" || task.status === "failed") && task.summary) {
@@ -447,98 +354,90 @@ function buildThread({ items, pendingMessages, response, isResponseActive, event
447
354
  }
448
355
  }
449
356
  entries.sort((first, second) => first.at - second.at);
450
- if (response?.status === "failed") {
451
- const userMessages = items.filter((item) => item.type === "message" && item.role === "user" && item.response_id === response.id);
452
- const lastUserMessage = userMessages[userMessages.length - 1];
453
- if (lastUserMessage) {
454
- entries.push({
455
- kind: "failure",
456
- id: `failure-${response.id}`,
457
- at: Date.parse(response.completed_at ?? lastUserMessage.created_at),
458
- message: response.error?.message ?? "Couldn’t finish",
459
- onRetry: () => retryMessage(lastUserMessage),
460
- });
461
- }
462
- }
463
- if (response?.status === "cancelled") {
464
- entries.push({
465
- kind: "line",
466
- id: `stopped-${response.id}`,
467
- at: Date.parse(response.completed_at ?? response.created_at),
468
- text: labels.stopped,
469
- });
470
- }
471
- for (const message of pendingMessages) {
472
- // A posted message the items already include would show twice until it is discarded.
473
- if (message.state === "posted" && items.some((item) => isSameMessage(item, message)))
474
- continue;
475
- entries.push({
476
- kind: "message",
477
- id: message.localId,
478
- side: "user",
479
- at: message.sentAt,
480
- body: message.content,
481
- attachments: toAttachments(message.attachments),
482
- sending: message.state === "sending",
483
- });
484
- if (message.state === "failed") {
485
- entries.push({
486
- kind: "failure",
487
- id: `failure-${message.localId}`,
488
- at: message.sentAt,
489
- message: sendError?.message ?? labels.notSent,
490
- onRetry: () => retryPending(message),
491
- });
357
+ const lastItem = items[items.length - 1];
358
+ const at = Math.max(lastItem?.at ?? 0, entries[entries.length - 1]?.at ?? 0) || Date.now();
359
+ const newestStep = runningSteps?.steps[runningSteps.steps.length - 1];
360
+ appendTurnEnding(entries, {
361
+ status,
362
+ retry,
363
+ failureMessage,
364
+ lastItem,
365
+ at,
366
+ labels,
367
+ doing: newestStep && `${newestStep.title}…`,
368
+ });
369
+ return entries;
370
+ function toEntries(item, inUnfinishedTurn) {
371
+ switch (item.kind) {
372
+ case "message": {
373
+ const assistant = item.role === "assistant";
374
+ const message = {
375
+ kind: "message",
376
+ id: item.id,
377
+ side: item.role,
378
+ at: item.at,
379
+ // Files sent alone keep an empty body, so the thread draws their tiles with no bubble.
380
+ body: item.text.trim() ? renderReply(item.text, !assistant) : "",
381
+ attachments: item.attachments,
382
+ sending: item.sending,
383
+ sources: assistant
384
+ ? [
385
+ ...splitSources(item.text).sources.map((source) => ({
386
+ id: source.id,
387
+ label: source.label,
388
+ href: source.href,
389
+ })),
390
+ ...(item.sources ?? []),
391
+ ]
392
+ : undefined,
393
+ actions: assistant && !inUnfinishedTurn ? replyActions?.(item.text) : undefined,
394
+ suggestedActions: item.suggestedActions,
395
+ footnote: item.footnote,
396
+ };
397
+ if (!item.failure)
398
+ return [message];
399
+ return [
400
+ message,
401
+ {
402
+ kind: "failure",
403
+ id: `failure-${item.id}`,
404
+ at: item.at,
405
+ title: labels.notSent,
406
+ message: item.failure.message,
407
+ onRetry: item.failure.retry,
408
+ },
409
+ ];
410
+ }
411
+ case "line":
412
+ return [{ kind: "line", id: item.id, at: item.at, text: item.text, tone: item.tone }];
413
+ case "card":
414
+ return [{ kind: "custom", id: item.id, side: "assistant", at: item.at, data: item.content }];
492
415
  }
493
416
  }
494
- const last = entries[entries.length - 1];
495
- if (isResponseActive &&
496
- response?.status !== "requires_action" &&
497
- !(last?.kind === "message" && last.side === "assistant")) {
498
- entries.push({ kind: "typing", id: "typing", at: last?.at ?? Date.now() });
499
- }
500
- return entries;
501
417
  }
502
- /** A tool call waiting for the user: what it would do, its arguments, and Deny or Allow. */
503
- function ApprovalCard({ item, onDecide, }) {
504
- const { labels } = useCiridaeLocale();
505
- const rows = listArgumentRows(item.arguments);
506
- return (_jsxs("div", { className: card, children: [_jsx("span", { children: describeApprovalRequest(item.tool) }), rows.length > 0 && (_jsxs("dl", { className: argumentList, children: [rows.slice(0, maximumArgumentRows).map(([name, value]) => (_jsxs(Fragment, { children: [_jsx("dt", { children: name }), _jsx("dd", { children: value })] }, name))), rows.length > maximumArgumentRows && (_jsxs("dd", { className: cardLabel, children: ["+", rows.length - maximumArgumentRows] }))] })), _jsxs("div", { className: cardActions, children: [_jsx(Button, { variant: "secondary", onClick: () => void onDecide(item, "deny"), children: labels.chat.deny }), _jsx(Button, { onClick: () => void onDecide(item, "allow"), children: labels.chat.allow })] })] }));
507
- }
508
- /** The assistant's questions, one option row per answer; picking one answers that question. */
509
- function QuestionCard({ item, onAnswer, }) {
510
- const [picked, setPicked] = useState([]);
511
- const questionIndex = picked.length;
512
- const question = item.questions[questionIndex];
513
- if (!question)
514
- return null;
515
- const pick = (label) => {
516
- const answers = [...picked, label];
517
- if (answers.length === item.questions.length) {
518
- void onAnswer(item, answers.map((answer) => [answer]));
418
+ /**
419
+ * The latest turn's foot: the agent thinking while it runs, saying what it is `doing`; its
420
+ * failure with Try again; or Stopped.
421
+ */
422
+ function appendTurnEnding(entries, { status, retry, failureMessage, lastItem, at, labels, doing, }) {
423
+ switch (status) {
424
+ case "running":
425
+ // Once its reply or a card waiting on the user is there, the agent is no longer thinking.
426
+ if (lastItem?.kind !== "card" &&
427
+ !(lastItem?.kind === "message" && lastItem.role === "assistant"))
428
+ entries.push({ kind: "typing", id: "typing", at, label: doing });
429
+ return;
430
+ case "failed": {
431
+ const message = failureMessage ?? labels.failed;
432
+ entries.push(retry
433
+ ? { kind: "failure", id: "turn-failure", at, message, onRetry: retry }
434
+ : { kind: "line", id: "turn-failure", at, text: message, tone: "error" });
435
+ return;
519
436
  }
520
- setPicked(answers);
521
- };
522
- return (_jsxs("div", { className: card, children: [_jsxs("span", { className: cardLabel, children: [question.header, item.questions.length > 1 && ` · ${questionIndex + 1} of ${item.questions.length}`] }), _jsx("span", { children: question.question }), _jsx("div", { className: optionList, children: question.options.map((option) => (_jsxs("button", { type: "button", onClick: () => pick(option.label), children: [option.label, option.description && _jsx("span", { className: optionDetail, children: option.description })] }, option.label))) })] }));
523
- }
524
- function isSameMessage(item, message) {
525
- return (item.type === "message" &&
526
- item.role === "user" &&
527
- item.content === message.content &&
528
- Date.parse(item.created_at) >= message.sentAt - 60000);
529
- }
530
- /** A call's JSON arguments as name and value pairs, values shortened to one line. */
531
- function listArgumentRows(argumentsJson) {
532
- try {
533
- const parsed = JSON.parse(argumentsJson);
534
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
535
- return [];
536
- return Object.entries(parsed).map(([name, value]) => [
537
- name.replace(/[_-]+/g, " "),
538
- typeof value === "string" ? value : JSON.stringify(value),
539
- ]);
540
- }
541
- catch {
542
- return [];
437
+ case "stopped":
438
+ entries.push({ kind: "line", id: "turn-stopped", at, text: labels.stopped });
439
+ return;
440
+ case "idle":
441
+ return;
543
442
  }
544
443
  }