@ciridae/ui 1.0.1 → 1.0.3

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 (141) 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 +75 -0
  21. package/dist/chat/conversation-source.js +1 -0
  22. package/dist/chat/message-text.d.ts +1 -1
  23. package/dist/chat/message-text.js +114 -46
  24. package/dist/chat/use-conversation-list.d.ts +6 -6
  25. package/dist/components/activity-feed.d.ts +2 -2
  26. package/dist/components/agent-work.d.ts +3 -3
  27. package/dist/components/app-frame.d.ts +79 -0
  28. package/dist/components/app-frame.js +92 -0
  29. package/dist/components/app-navigation.d.ts +3 -3
  30. package/dist/components/badge.d.ts +1 -1
  31. package/dist/components/brand-icons.d.ts +3 -3
  32. package/dist/components/bulk-action-bar.d.ts +1 -1
  33. package/dist/components/button.d.ts +4 -4
  34. package/dist/components/calendar.d.ts +1 -1
  35. package/dist/components/centered-page.d.ts +1 -1
  36. package/dist/components/chart.d.ts +22 -2
  37. package/dist/components/chart.js +239 -44
  38. package/dist/components/chat-client-source.d.ts +16 -0
  39. package/dist/components/chat-client-source.js +256 -0
  40. package/dist/components/chat-page.d.ts +9 -8
  41. package/dist/components/chat-page.js +7 -11
  42. package/dist/components/chat-sheet-controller.d.ts +10 -14
  43. package/dist/components/chat-sheet-controller.js +2 -3
  44. package/dist/components/chat-sheet.d.ts +10 -11
  45. package/dist/components/chat-sheet.js +135 -273
  46. package/dist/components/chat.d.ts +22 -8
  47. package/dist/components/chat.js +128 -33
  48. package/dist/components/checkbox-item.d.ts +1 -1
  49. package/dist/components/checkbox-list.d.ts +1 -1
  50. package/dist/components/chip.d.ts +1 -1
  51. package/dist/components/collapsible.d.ts +1 -1
  52. package/dist/components/color-theme.d.ts +1 -1
  53. package/dist/components/column-settings.d.ts +1 -1
  54. package/dist/components/command-menu.d.ts +1 -1
  55. package/dist/components/command-palette.d.ts +3 -3
  56. package/dist/components/command-palette.js +1 -2
  57. package/dist/components/copyable-id.d.ts +22 -5
  58. package/dist/components/copyable-id.js +46 -6
  59. package/dist/components/data-display.d.ts +4 -4
  60. package/dist/components/data-grid.d.ts +10 -10
  61. package/dist/components/date-range-picker.d.ts +1 -1
  62. package/dist/components/date-time.d.ts +1 -1
  63. package/dist/components/dialog-height-context.d.ts +1 -1
  64. package/dist/components/document-viewer.d.ts +2 -2
  65. package/dist/components/drawer.d.ts +6 -6
  66. package/dist/components/dropzone.d.ts +4 -4
  67. package/dist/components/editable-field.d.ts +2 -2
  68. package/dist/components/email-input.d.ts +1 -1
  69. package/dist/components/environment-frame.d.ts +1 -1
  70. package/dist/components/expandable-text.d.ts +1 -1
  71. package/dist/components/feedback-screenshot.d.ts +1 -1
  72. package/dist/components/feedback-sheet.d.ts +18 -3
  73. package/dist/components/feedback-sheet.js +216 -55
  74. package/dist/components/field-context.d.ts +1 -1
  75. package/dist/components/filter-bar.d.ts +3 -3
  76. package/dist/components/filter-bar.js +4 -1
  77. package/dist/components/form-controls.d.ts +4 -4
  78. package/dist/components/form-layout.d.ts +7 -7
  79. package/dist/components/hotkeys.d.ts +2 -2
  80. package/dist/components/icon.d.ts +2 -2
  81. package/dist/components/input.d.ts +4 -4
  82. package/dist/components/input.js +3 -2
  83. package/dist/components/keyboard-shortcuts.d.ts +2 -2
  84. package/dist/components/keycap.d.ts +2 -2
  85. package/dist/components/layout.d.ts +15 -15
  86. package/dist/components/lightbox.d.ts +9 -3
  87. package/dist/components/lightbox.js +11 -8
  88. package/dist/components/link.d.ts +1 -1
  89. package/dist/components/locale.d.ts +32 -2
  90. package/dist/components/locale.js +25 -1
  91. package/dist/components/log-list.d.ts +3 -3
  92. package/dist/components/mention-input.d.ts +2 -2
  93. package/dist/components/menu.d.ts +2 -2
  94. package/dist/components/meter.d.ts +1 -1
  95. package/dist/components/navigation.d.ts +2 -2
  96. package/dist/components/notification-tray.d.ts +1 -1
  97. package/dist/components/overlays.d.ts +4 -4
  98. package/dist/components/page-frame.d.ts +4 -4
  99. package/dist/components/panel.d.ts +4 -4
  100. package/dist/components/profile-header.d.ts +1 -1
  101. package/dist/components/progress.d.ts +1 -1
  102. package/dist/components/project-avatar.d.ts +1 -1
  103. package/dist/components/radio-card.d.ts +2 -2
  104. package/dist/components/record.d.ts +4 -4
  105. package/dist/components/resolved-file-url.d.ts +19 -0
  106. package/dist/components/resolved-file-url.js +49 -0
  107. package/dist/components/scroll-area.d.ts +1 -1
  108. package/dist/components/secret-dialog.d.ts +1 -1
  109. package/dist/components/segmented-bar.d.ts +1 -1
  110. package/dist/components/select.d.ts +4 -4
  111. package/dist/components/settings.d.ts +4 -4
  112. package/dist/components/shell-skeleton.d.ts +1 -1
  113. package/dist/components/shell.d.ts +19 -19
  114. package/dist/components/shell.js +3 -1
  115. package/dist/components/side-panel.d.ts +3 -3
  116. package/dist/components/sidebar.d.ts +6 -6
  117. package/dist/components/site-banner.d.ts +2 -2
  118. package/dist/components/skeleton.d.ts +1 -1
  119. package/dist/components/skeletons.d.ts +5 -5
  120. package/dist/components/stages.d.ts +5 -5
  121. package/dist/components/stat-card.d.ts +3 -3
  122. package/dist/components/status-banner.d.ts +1 -1
  123. package/dist/components/status.d.ts +2 -2
  124. package/dist/components/steps.d.ts +1 -1
  125. package/dist/components/switch.d.ts +1 -1
  126. package/dist/components/tabs.d.ts +8 -8
  127. package/dist/components/theme-customizer.d.ts +2 -2
  128. package/dist/components/toast.d.ts +1 -1
  129. package/dist/components/use-new-version-available.d.ts +6 -0
  130. package/dist/components/use-new-version-available.js +29 -0
  131. package/dist/components/user-chip.d.ts +3 -3
  132. package/dist/components/wizard.d.ts +2 -2
  133. package/dist/feedback/contract.d.ts +136 -136
  134. package/dist/feedback/feedback-controller.d.ts +6 -6
  135. package/dist/feedback/generated/zod.gen.d.ts +28 -28
  136. package/dist/feedback/operator-contract.d.ts +11 -11
  137. package/dist/feedback/operator-controller.d.ts +32 -13
  138. package/dist/feedback/operator-controller.js +44 -11
  139. package/dist/styled-system/styles.css +203 -59
  140. package/dist/theme/tokens.js +17 -8
  141. package/package.json +32 -3
@@ -2,32 +2,32 @@ 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, 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";
29
28
  import { heading } from "./layout.js";
30
29
  import { useCiridaeLocale } from "./locale.js";
30
+ export { useChatClientConversations, useChatClientSource, } from "./chat-client-source.js";
31
31
  /* The docked aside, drawn like the shell's main panel beside it. */
32
32
  const docked = css({
33
33
  display: "flex",
@@ -133,7 +133,7 @@ const dockedHeader = css({
133
133
  paddingInlineEnd: "panel.default",
134
134
  borderBottom: "1px solid token(colors.border.control)",
135
135
  });
136
- const headerActions = css({ display: "flex", alignItems: "center", gap: "layout.controlGap" });
136
+ const headerActions = css({ display: "flex", alignItems: "center", gap: "layout.tightGap" });
137
137
  const titleLine = css({
138
138
  display: "inline-flex",
139
139
  alignItems: "center",
@@ -166,54 +166,7 @@ const contextChip = css({
166
166
  });
167
167
  /* The thread's scrolling area, between the header and the composer. */
168
168
  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" });
169
+ const centered = css({ display: "grid", placeItems: "center", height: "100%" });
217
170
  /* The thread and composer, the drop target for files; it fills what the sheet or page gives it. */
218
171
  const conversationRoot = css({
219
172
  display: "flex",
@@ -230,13 +183,12 @@ const composerFooter = css({
230
183
  borderTop: "1px solid token(colors.border.control)",
231
184
  "&[data-layout=wide]": { borderTop: "0", paddingBlockEnd: "panel.default" },
232
185
  });
186
+ /* Over the composer, what goes with it: an error. */
187
+ const footerStack = css({ display: "grid", gap: "layout.tightGap", minWidth: "0" });
233
188
  /* Full page: the thread and composer in one readable column down the middle. */
234
189
  const wideColumn = css({ width: "100%", maxWidth: "chat.thread", marginInline: "auto" });
235
- const hiddenInput = css({ display: "none" });
236
190
  /* In a drawer, the context row and the conversation stack under its header. */
237
191
  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
192
  export function ChatSheet({ open, onOpenChange, title, context, placement = "drawer", onPlacementChange, onOpenFullPage, ...conversationProps }) {
241
193
  const { variant = "agent", tasks } = conversationProps;
242
194
  const { labels } = useCiridaeLocale();
@@ -245,7 +197,7 @@ export function ChatSheet({ open, onOpenChange, title, context, placement = "dra
245
197
  const titleContent = (_jsxs("span", { className: titleLine, children: [_jsx(SparkleIcon, { className: icon({ size: "small" }), weight: "fill" }), title] }));
246
198
  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 &&
247
199
  (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 })] }));
200
+ 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": "", children: [item.icon, item.label] }, item.label)))] })), variant === "agent" && tasks && _jsx(AgentWork, { layout: "summary", tasks: tasks })] }));
249
201
  if (placement === "docked") {
250
202
  if (!open)
251
203
  return null;
@@ -254,15 +206,17 @@ export function ChatSheet({ open, onOpenChange, title, context, placement = "dra
254
206
  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
207
  }
256
208
  /* The files for the next message: each uploads as it is added; send waits until all have. */
257
- function useComposerFiles(client) {
209
+ function useComposerFiles(upload, onUploadError) {
258
210
  const [entries, setEntries] = useState([]);
259
211
  const remove = (file) => setEntries((existing) => existing.filter((entry) => entry.file !== file));
260
212
  const add = (files) => {
213
+ if (!upload)
214
+ return;
261
215
  setEntries((existing) => [...existing, ...files.map((file) => ({ file, attachment: null }))]);
262
216
  for (const file of files) {
263
- void client.uploadAttachment(file).then((attachment) => setEntries((existing) => existing.map((entry) => (entry.file === file ? { file, attachment } : entry))), (error) => {
217
+ upload(file).then((attachment) => setEntries((existing) => existing.map((entry) => (entry.file === file ? { file, attachment } : entry))), (error) => {
264
218
  remove(file);
265
- throw error;
219
+ onUploadError(error);
266
220
  });
267
221
  }
268
222
  };
@@ -279,54 +233,53 @@ function useComposerFiles(client) {
279
233
  * The thread and the composer: everything that reads or writes the conversation. `layout="wide"`
280
234
  * sets both in one readable column for a full page; the sheet uses `sheet`.
281
235
  */
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", }) {
236
+ export function ChatConversation({ source, placeholder, draft, onDraftChange, composerRef, mentions, mentionMark, onMentionSelect, mentionHref, events = [], assistantName: ownAssistantName, suggestions, copyReplies = false, variant = "agent", tasks, onTaskReply, layout = "sheet", scrollBody: ScrollBody = "div", }) {
283
237
  const { labels } = useCiridaeLocale();
284
238
  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);
239
+ const [ownDraft, setOwnDraft] = useState("");
240
+ const currentDraft = draft ?? ownDraft;
241
+ const setDraft = (next) => {
242
+ setOwnDraft(next);
243
+ onDraftChange?.(next);
244
+ };
245
+ const [sending, setSending] = useState(false);
246
+ const [composerError, setComposerError] = useState(null);
247
+ const showError = (error) => setComposerError(error instanceof Error ? error.message : String(error));
248
+ const files = useComposerFiles(source.upload, showError);
249
+ const send = async (text) => {
250
+ setSending(true);
251
+ setComposerError(null);
295
252
  try {
296
- await state.sendMessage(content, sentAttachments);
253
+ await source.send({ text, attachments: files.attachments });
254
+ files.clear();
297
255
  }
298
256
  catch (error) {
299
- setSendError(error instanceof Error ? error : new Error(String(error)));
257
+ // The composer cleared as it sent: the text comes back, with the files still waiting.
258
+ setDraft(text);
259
+ showError(error);
260
+ }
261
+ finally {
262
+ setSending(false);
300
263
  }
301
264
  };
302
- const renderReply = (content, plain) => (_jsx(MessageText, { content: content, plain: plain, onMentionSelect: onMentionSelect, mentionHref: mentionHref }));
303
265
  const entries = buildThread({
304
266
  labels: labels.chat,
305
- items: state.items,
306
- pendingMessages: state.pendingMessages,
307
- response: state.response,
308
- isResponseActive: state.isResponseActive,
267
+ source,
309
268
  events,
310
269
  tasks: variant === "agent" ? (tasks ?? []) : [],
311
- sendError,
312
- renderReply,
270
+ renderReply: (content, plain) => (_jsx(MessageText, { content: content, plain: plain, onMentionSelect: onMentionSelect, mentionHref: mentionHref })),
313
271
  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),
272
+ taskCard: (task) => _jsx(AgentTaskCard, { task: task, onReply: onTaskReply }),
320
273
  });
321
274
  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
- } })] }) }) }) })] }));
275
+ const showSuggestions = entries.length === 0 && !source.loading && suggestions !== undefined && suggestions.length > 0;
276
+ 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, density: "compact", 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: source.upload && _jsx(AttachFilesButton, {}) })] }) })] }));
277
+ }
278
+ /** The paperclip: the enclosing `Dropzone`'s file picker. */
279
+ function AttachFilesButton() {
280
+ const { labels } = useCiridaeLocale();
281
+ const picker = useDropzonePicker();
282
+ return (_jsx(IconButton, { "aria-label": labels.chat.attachFile, size: "compact", variant: "ghost", disabled: picker.disabled, onClick: picker.openPicker, children: _jsx(PaperclipIcon, { className: icon({ size: "small" }) }) }));
330
283
  }
331
284
  /** Copies a reply's text, its mentions as their labels, and shows a tick for a moment. */
332
285
  function CopyReplyButton({ content }) {
@@ -334,88 +287,23 @@ function CopyReplyButton({ content }) {
334
287
  .map((part) => (typeof part === "string" ? part : part.label))
335
288
  .join("") }));
336
289
  }
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, }) {
290
+ /**
291
+ * The source's items, the page's events, the agent's tasks and how the latest turn stands, in one
292
+ * timeline: the turn's typing, failure or stop at its foot.
293
+ */
294
+ function buildThread({ labels, source: { items, status, failureMessage, retry }, events, tasks, renderReply, replyActions, taskCard, }) {
339
295
  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;
417
- }
418
- }
296
+ // The latest turn is everything after the user's last message; its replies are done once it is.
297
+ let latestTurnStart = -1;
298
+ items.forEach((item, index) => {
299
+ if (item.kind === "message" && item.role === "user")
300
+ latestTurnStart = index;
301
+ });
302
+ const latestTurnUnfinished = status === "running" || status === "failed";
303
+ items.forEach((item, index) => {
304
+ const inUnfinishedTurn = latestTurnUnfinished && index > latestTurnStart;
305
+ entries.push(...toEntries(item, inUnfinishedTurn));
306
+ });
419
307
  for (const event of events) {
420
308
  entries.push({
421
309
  kind: "line",
@@ -432,7 +320,7 @@ function buildThread({ items, pendingMessages, response, isResponseActive, event
432
320
  id: `task-${task.id}`,
433
321
  side: "assistant",
434
322
  at: Date.parse(task.ask.askedAt),
435
- data: { kind: "task", task },
323
+ data: taskCard(task),
436
324
  });
437
325
  }
438
326
  if ((task.status === "done" || task.status === "failed") && task.summary) {
@@ -447,98 +335,72 @@ function buildThread({ items, pendingMessages, response, isResponseActive, event
447
335
  }
448
336
  }
449
337
  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
- });
338
+ const lastItem = items[items.length - 1];
339
+ const at = Math.max(lastItem?.at ?? 0, entries[entries.length - 1]?.at ?? 0) || Date.now();
340
+ appendTurnEnding(entries, { status, retry, failureMessage, lastItem, at, labels });
341
+ return entries;
342
+ function toEntries(item, inUnfinishedTurn) {
343
+ switch (item.kind) {
344
+ case "message": {
345
+ const assistant = item.role === "assistant";
346
+ const message = {
347
+ kind: "message",
348
+ id: item.id,
349
+ side: item.role,
350
+ at: item.at,
351
+ // Files sent alone keep an empty body, so the thread draws their tiles with no bubble.
352
+ body: item.text.trim() ? renderReply(item.text, !assistant) : "",
353
+ attachments: item.attachments,
354
+ sending: item.sending,
355
+ sources: assistant
356
+ ? splitSources(item.text).sources.map((source) => ({
357
+ id: source.id,
358
+ label: source.label,
359
+ href: source.href,
360
+ }))
361
+ : undefined,
362
+ actions: assistant && !inUnfinishedTurn ? replyActions?.(item.text) : undefined,
363
+ };
364
+ if (!item.failure)
365
+ return [message];
366
+ return [
367
+ message,
368
+ {
369
+ kind: "failure",
370
+ id: `failure-${item.id}`,
371
+ at: item.at,
372
+ message: item.failure.message ?? labels.notSent,
373
+ onRetry: item.failure.retry,
374
+ },
375
+ ];
376
+ }
377
+ case "line":
378
+ return [{ kind: "line", id: item.id, at: item.at, text: item.text, tone: item.tone }];
379
+ case "card":
380
+ return [{ kind: "custom", id: item.id, side: "assistant", at: item.at, data: item.content }];
492
381
  }
493
382
  }
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
- }
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
383
  }
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]));
384
+ /** The latest turn's foot: the agent typing while it runs, its failure with Retry, or Stopped. */
385
+ function appendTurnEnding(entries, { status, retry, failureMessage, lastItem, at, labels, }) {
386
+ switch (status) {
387
+ case "running":
388
+ // Once its reply or a card waiting on the user is there, the agent is no longer typing.
389
+ if (lastItem?.kind !== "card" &&
390
+ !(lastItem?.kind === "message" && lastItem.role === "assistant"))
391
+ entries.push({ kind: "typing", id: "typing", at });
392
+ return;
393
+ case "failed": {
394
+ const message = failureMessage ?? labels.failed;
395
+ entries.push(retry
396
+ ? { kind: "failure", id: "turn-failure", at, message, onRetry: retry }
397
+ : { kind: "line", id: "turn-failure", at, text: message, tone: "error" });
398
+ return;
519
399
  }
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 [];
400
+ case "stopped":
401
+ entries.push({ kind: "line", id: "turn-stopped", at, text: labels.stopped });
402
+ return;
403
+ case "idle":
404
+ return;
543
405
  }
544
406
  }
@@ -1,6 +1,9 @@
1
1
  import { type ComponentProps, type ReactNode } from "react";
2
2
  import { type MessageSide } from "../chat/message-group-position.js";
3
3
  import { type MentionMark, type MentionTrigger } from "./mention-input.js";
4
+ import { type ResolveFileUrl } from "./resolved-file-url.js";
5
+ export { MessageText, splitSources, type MessageSource, type MessageTextProps, } from "../chat/message-text.js";
6
+ export type { ResolvableFile, ResolveFileUrl } from "./resolved-file-url.js";
4
7
  export type ChatComposerProps = Omit<ComponentProps<"div">, "onSubmit"> & {
5
8
  /** The text, each mention in it as `[@label](mention://kind/id)`. */
6
9
  value?: string;
@@ -18,6 +21,11 @@ export type ChatComposerProps = Omit<ComponentProps<"div">, "onSubmit"> & {
18
21
  sendsWithoutText?: boolean;
19
22
  /** What goes with the message, such as its files as chips, just above the text. */
20
23
  attachments?: ReactNode;
24
+ /**
25
+ * Files pasted into the composer, such as a screenshot or files copied from the desktop, to
26
+ * attach as a drop or the picker would. Rich text pasted with a picture of itself stays text.
27
+ */
28
+ onPasteFiles?: (files: File[]) => void;
21
29
  /**
22
30
  * `page` is a chat page's wide card; `sheet` the field at the foot of the agent sheet; `feed`
23
31
  * a comment field, which sends on Cmd/Ctrl+Enter rather than Enter.
@@ -33,21 +41,21 @@ export type ChatComposerProps = Omit<ComponentProps<"div">, "onSubmit"> & {
33
41
  /** Props for the text field itself, such as a ref. */
34
42
  inputProps?: Omit<ComponentProps<"div">, "children" | "defaultValue" | "onChange" | "onSubmit" | "placeholder">;
35
43
  };
36
- export declare function ChatComposer({ value, defaultValue, onValueChange, onSend, placeholder, ariaLabel: ownAriaLabel, tools, actions, submitLabel: ownSubmitLabel, sendDisabled, sendsWithoutText, attachments, variant, autoFocus, mentions, mentionMark, mentionsLabel, inputProps, className, ...props }: ChatComposerProps): import("@types/react").JSX.Element;
44
+ export declare function ChatComposer({ value, defaultValue, onValueChange, onSend, placeholder, ariaLabel: ownAriaLabel, tools, actions, submitLabel: ownSubmitLabel, sendDisabled, sendsWithoutText, attachments, onPasteFiles, variant, autoFocus, mentions, mentionMark, mentionsLabel, inputProps, className, ...props }: ChatComposerProps): import("react").JSX.Element;
37
45
  export type ChatBubbleProps = ComponentProps<"div"> & {
38
46
  side: "user" | "assistant";
39
47
  /** Where the bubble sits in its run of messages from one side; see `listMessageGroupPositions`. */
40
48
  groupPosition?: "single" | "first" | "middle" | "last";
41
49
  };
42
50
  /** A message bubble in the agent sheet. */
43
- export declare function ChatBubble({ side, groupPosition, className, ...props }: ChatBubbleProps): import("@types/react").JSX.Element;
51
+ export declare function ChatBubble({ side, groupPosition, className, ...props }: ChatBubbleProps): import("react").JSX.Element;
44
52
  /** A centered line between bubbles: a finished tool call, a decision, something the page did. */
45
53
  export declare function ChatSystemLine({ tone, className, ...props }: ComponentProps<"div"> & {
46
54
  tone?: "muted" | "error";
47
- }): import("@types/react").JSX.Element;
55
+ }): import("react").JSX.Element;
48
56
  /** Three pulsing dots: the assistant is typing. */
49
- export declare function ChatTypingDots({ className, ...props }: ComponentProps<"span">): import("@types/react").JSX.Element;
50
- export declare function ChatAction({ className, ...props }: ComponentProps<"button">): import("@types/react").JSX.Element;
57
+ export declare function ChatTypingDots({ className, ...props }: ComponentProps<"span">): import("react").JSX.Element;
58
+ export declare function ChatAction({ className, ...props }: ComponentProps<"button">): import("react").JSX.Element;
51
59
  /** Who wrote a message: drawn as the avatar beside its group and named in the group's footer. */
52
60
  export type ChatAuthor = {
53
61
  name: string;
@@ -58,7 +66,8 @@ export type ChatAuthor = {
58
66
  };
59
67
  /**
60
68
  * A file sent with a message, linked when it has a `url`. With the thread's `attachmentTiles`, an
61
- * image (`mediaType` `image/*`) with a `url` shows as a thumbnail.
69
+ * image (`mediaType` `image/*`) shows as a thumbnail; one with no `url` loads through the thread's
70
+ * `resolveAttachmentUrl`.
62
71
  */
63
72
  export type ChatAttachment = {
64
73
  id: string;
@@ -123,13 +132,18 @@ export type ChatThreadProps<Custom = never> = {
123
132
  attachmentTiles?: boolean;
124
133
  /** Heads each day's entries with its date, for a thread that runs over days. */
125
134
  dayLabels?: boolean;
135
+ /**
136
+ * Loads the link of an attachment with no `url`, such as a file the service serves only with a
137
+ * bearer token, for its thumbnail and the lightbox; see `ResolveFileUrl`.
138
+ */
139
+ resolveAttachmentUrl?: ResolveFileUrl<ChatAttachment>;
126
140
  className?: string;
127
141
  };
128
- export declare function ChatThread<Custom = never>({ entries, assistant, renderCustom, formatTime, density, attachmentTiles, dayLabels, className, }: ChatThreadProps<Custom>): import("@types/react").JSX.Element;
142
+ export declare function ChatThread<Custom = never>({ entries, assistant, renderCustom, formatTime, density, attachmentTiles, dayLabels, resolveAttachmentUrl, className, }: ChatThreadProps<Custom>): import("react").JSX.Element;
129
143
  export type ChatSuggestionsProps = {
130
144
  suggestions: readonly string[];
131
145
  onSelect: (suggestion: string) => void;
132
146
  className?: string;
133
147
  };
134
148
  /** Prompts to start a conversation from, stacked at the foot of an empty thread. */
135
- export declare function ChatSuggestions({ suggestions, onSelect, className }: ChatSuggestionsProps): import("@types/react").JSX.Element;
149
+ export declare function ChatSuggestions({ suggestions, onSelect, className }: ChatSuggestionsProps): import("react").JSX.Element;