@ciridae/ui 1.0.3 → 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 (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 +105 -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 +76 -39
  26. package/dist/components/chat.d.ts +90 -14
  27. package/dist/components/chat.js +402 -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 +12 -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 +289 -33
  111. package/dist/styled-system/styles.css +3115 -1044
  112. package/dist/styled-system/tokens/index.mjs +598 -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 +107 -32
  124. package/package.json +9 -1
@@ -28,7 +28,7 @@ export declare function TimeSeriesChart({ series, start, end, formatValue, forma
28
28
  }): import("react").JSX.Element;
29
29
  /**
30
30
  * Values by category as bars, side by side per series, or as lines, such as counts per month.
31
- * Each series takes the next accent; a legend names them once there is more than one.
31
+ * Each series takes the next chart colour; a legend names them once there is more than one.
32
32
  */
33
33
  export declare function CategoryChart({ categories, series, variant, formatValue, height, allowDecimals, className, ...props }: ComponentProps<"div"> & {
34
34
  categories: readonly string[];
@@ -20,25 +20,30 @@ const frame = css({
20
20
  color: "fg.muted",
21
21
  fontFamily: "numeric",
22
22
  fontSize: "caption",
23
- "--series-0": "token(colors.accent.blue)",
24
- "--series-1": "token(colors.accent.green)",
25
- "--series-2": "token(colors.accent.yellow)",
26
- "--series-3": "token(colors.accent.bronze)",
27
- "--series-4": "token(colors.accent.red)",
23
+ // The series that matters is the trend's blue, a bar the lighter bar blue; the rest take the
24
+ // chart's four categories in their fixed order.
25
+ "--series-0": "token(colors.chart.trend)",
26
+ "--series-1": "token(colors.chart.category1)",
27
+ "--series-2": "token(colors.chart.category2)",
28
+ "--series-3": "token(colors.chart.category3)",
29
+ "--series-4": "token(colors.chart.category4)",
30
+ "&[data-variant=bar]": { "--series-0": "token(colors.chart.bar)" },
28
31
  "--chart-grid": "token(colors.border.subtle)",
29
32
  "--chart-muted": "token(colors.fg.muted)",
30
33
  "--chart-cursor": "token(colors.bg.hover)",
31
34
  "& .ts-chart-host": { overflow: "visible" },
32
35
  });
33
- // The tooltip's own React body replaces the chart's default chrome, so it draws the card itself.
36
+ // The tooltip's own React body replaces the chart's default chrome, so it draws the popover's
37
+ // glass itself.
34
38
  const tooltipBody = css({
35
39
  display: "grid",
36
40
  gap: "control.trackInset",
37
41
  padding: "panel.compact",
38
42
  border: "1px solid token(colors.border.control)",
39
43
  borderRadius: "action",
40
- background: "bg.card",
41
- boxShadow: "card",
44
+ background: "bg.popover",
45
+ backdropFilter: "blur(token(blurs.popover))",
46
+ boxShadow: "soft",
42
47
  color: "fg.primary",
43
48
  fontFamily: "sans",
44
49
  fontSize: "caption",
@@ -185,7 +190,7 @@ export function TimeSeriesChart({ series, start, end, formatValue, formatTimesta
185
190
  }
186
191
  /**
187
192
  * Values by category as bars, side by side per series, or as lines, such as counts per month.
188
- * Each series takes the next accent; a legend names them once there is more than one.
193
+ * Each series takes the next chart colour; a legend names them once there is more than one.
189
194
  */
190
195
  export function CategoryChart({ categories, series, variant = "bar", formatValue = String, height = 176, allowDecimals = true, className, ...props }) {
191
196
  const definition = useMemo(() => {
@@ -257,9 +262,9 @@ export function CategoryChart({ categories, series, variant = "bar", formatValue
257
262
  tooltip: seriesTooltip(series, formatValue, String),
258
263
  });
259
264
  }, [allowDecimals, categories, formatValue, series, variant]);
260
- return (_jsxs("div", { ...props, className: cx(frame, className), "data-ciridae-ui": "", children: [_jsx(Chart, { ariaLabel: series.map((item) => item.label).join(", "), definition: definition, height: height, renderTooltipBody: ({ points, content }) => (_jsx(SeriesTooltip, { content: content, points: points, series: series, formatValue: formatValue })) }), _jsx(SeriesLegend, { series: series })] }));
265
+ return (_jsxs("div", { ...props, className: cx(frame, className), "data-ciridae-ui": "", "data-variant": variant, children: [_jsx(Chart, { ariaLabel: series.map((item) => item.label).join(", "), definition: definition, height: height, renderTooltipBody: ({ points, content }) => (_jsx(SeriesTooltip, { content: content, points: points, series: series, formatValue: formatValue })) }), _jsx(SeriesLegend, { series: series })] }));
261
266
  }
262
- /** Each series index to its accent, in order. */
267
+ /** Each series index to its chart colour, in order. */
263
268
  function seriesColorScale(count) {
264
269
  return scaleOrdinal()
265
270
  .domain(Array.from({ length: count }, (_, index) => index))
@@ -6,7 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  */
7
7
  import { Fragment, useMemo, useRef, useState } from "react";
8
8
  import { css } from "#primitives/css";
9
- import { describeApprovalRequest } from "../chat/describe-tool-call.js";
9
+ import { describeToolCall } from "../chat/describe-tool-call.js";
10
10
  import { useChatConversation } from "../chat/use-chat-conversation.js";
11
11
  import { useConversationList } from "../chat/use-conversation-list.js";
12
12
  import { Button } from "./button.js";
@@ -93,10 +93,16 @@ export function useChatClientSource({ client, conversationId, onConversationCrea
93
93
  ? items.filter((item) => item.type === "message" && item.role === "user" && item.response_id === response.id)
94
94
  : [];
95
95
  const lastUserMessage = failedTurnMessages[failedTurnMessages.length - 1];
96
+ // How each tool call ended, by its call id; a call with none has not returned yet.
97
+ const callStatuses = new Map(items.flatMap((item) => item.type === "function_call_output" ? [[item.call_id, item.status]] : []));
98
+ const lastItem = items[items.length - 1];
96
99
  const conversationItems = [];
97
100
  for (const item of items) {
98
101
  const converted = toConversationItem(item, {
99
102
  waitingOnUser: response?.id === item.response_id && response.status === "requires_action",
103
+ inRunningTurn: status === "running" && response?.id === item.response_id,
104
+ newest: item === lastItem,
105
+ callStatuses,
100
106
  toChatAttachment,
101
107
  answered: labels.chat.answered,
102
108
  decided: (decision) => (decision === "allow" ? labels.chat.allowed : labels.chat.denied),
@@ -158,8 +164,11 @@ export function useChatClientConversations(client) {
158
164
  }))), [list.data]);
159
165
  return { conversations, loading: list.isPending };
160
166
  }
161
- /** An item as the thread draws it; what the agent runs on its way to a reply stays behind. */
162
- function toConversationItem(item, { waitingOnUser, toChatAttachment, answered, decided, decideApproval, answerQuestion, }) {
167
+ /**
168
+ * An item as the thread draws it. A tool call is a step named by its tool, with how it ended and
169
+ * neither its arguments nor its output; a thought is a step named by its first line.
170
+ */
171
+ function toConversationItem(item, { waitingOnUser, inRunningTurn, newest, callStatuses, toChatAttachment, answered, decided, decideApproval, answerQuestion, }) {
163
172
  const at = Date.parse(item.created_at);
164
173
  switch (item.type) {
165
174
  case "message":
@@ -177,7 +186,7 @@ function toConversationItem(item, { waitingOnUser, toChatAttachment, answered, d
177
186
  kind: "line",
178
187
  id: item.id,
179
188
  at,
180
- text: `${describeApprovalRequest(item.tool)} · ${decided(item.decision)}`,
189
+ text: `${describeToolCall(item.tool)} · ${decided(item.decision)}`,
181
190
  };
182
191
  }
183
192
  return waitingOnUser
@@ -206,16 +215,48 @@ function toConversationItem(item, { waitingOnUser, toChatAttachment, answered, d
206
215
  }
207
216
  : null;
208
217
  case "function_call":
218
+ // Only the tool's name and how the call ended: its arguments can hold values, such as a
219
+ // credential or a customer's details, that anyone reading the thread should not see.
220
+ return {
221
+ kind: "step",
222
+ id: item.id,
223
+ at,
224
+ step: {
225
+ kind: "tool",
226
+ title: describeToolCall(item.name),
227
+ // A call that never returned from a finished response did not run to its end.
228
+ status: callStatuses.get(item.call_id) ?? (inRunningTurn ? "running" : "failed"),
229
+ },
230
+ };
209
231
  case "function_call_output":
210
- case "reasoning":
211
232
  return null;
233
+ case "reasoning": {
234
+ const [title = "", ...rest] = item.content
235
+ .split("\n")
236
+ .map((line) => line.trim())
237
+ .filter(Boolean);
238
+ if (!title)
239
+ return null;
240
+ return {
241
+ kind: "step",
242
+ id: item.id,
243
+ at,
244
+ step: {
245
+ kind: "reasoning",
246
+ // A thought often opens with its gist in bold, as a heading.
247
+ title: title.replace(/^\*\*(.+)\*\*$/, "$1"),
248
+ detail: rest.join(" ") || undefined,
249
+ status: inRunningTurn && newest ? "running" : "completed",
250
+ },
251
+ };
252
+ }
212
253
  }
213
254
  }
214
255
  /** A tool call waiting for the user: what it would do, its arguments, and Deny or Allow. */
215
256
  function ApprovalCard({ item, onDecide, }) {
216
257
  const { labels } = useCiridaeLocale();
217
258
  const rows = listArgumentRows(item.arguments);
218
- 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 })] })] }));
259
+ return (_jsxs("div", { className: card, children: [_jsx("span", { children: describeToolCall(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 })] })] }));
219
260
  }
220
261
  /** The assistant's questions, one option row per answer; picking one answers that question. */
221
262
  function QuestionCard({ item, onAnswer, }) {
@@ -33,6 +33,7 @@ export declare function useChatSheetController({ hotkey, defaultPlacement, docke
33
33
  placement: ChatSheetPlacement;
34
34
  onPlacementChange: ((next: ChatSheetPlacement) => void) | undefined;
35
35
  onOpenFullPage: (() => void) | undefined;
36
+ shortcut: HotkeyChord;
36
37
  draft: string;
37
38
  onDraftChange: import("react").Dispatch<import("react").SetStateAction<string>>;
38
39
  composerRef: import("react").RefObject<HTMLDivElement | null>;
@@ -123,6 +123,7 @@ export function useChatSheetController({ hotkey = "Mod+J", defaultPlacement = "d
123
123
  placement,
124
124
  onPlacementChange: dockable ? changePlacement : undefined,
125
125
  onOpenFullPage: openFullPage,
126
+ shortcut: hotkey,
126
127
  draft,
127
128
  onDraftChange: setDraft,
128
129
  composerRef,
@@ -1,6 +1,8 @@
1
1
  import { type ReactNode, type Ref } from "react";
2
2
  import { type AgentTask, type AgentTaskReply } from "./agent-work.js";
3
+ import { type ChatDictation } from "./chat.js";
3
4
  import { DrawerBody } from "./drawer.js";
5
+ import type { HotkeyChord } from "./hotkeys.js";
4
6
  import type { MentionMark, MentionTrigger } from "./mention-input.js";
5
7
  import type { Mention } from "../chat/contract.js";
6
8
  import type { ChatConversationSource } from "../chat/conversation-source.js";
@@ -53,6 +55,10 @@ export type ChatConversationProps = {
53
55
  suggestions?: readonly string[];
54
56
  /** A Copy control in the footer of each reply once its turn has finished, and not failed. */
55
57
  copyReplies?: boolean;
58
+ /** More tools in the composer's bar after the paperclip, such as a dictation button. */
59
+ composerTools?: ReactNode;
60
+ /** While the host takes dictation, its levels as the composer's waveform; see `ChatComposer`. */
61
+ dictation?: ChatDictation;
56
62
  };
57
63
  export type ChatSheetProps = ChatConversationProps & {
58
64
  open: boolean;
@@ -66,13 +72,15 @@ export type ChatSheetProps = ChatConversationProps & {
66
72
  onPlacementChange?: (placement: ChatSheetPlacement) => void;
67
73
  /** Shows a header button that moves the conversation to a full page. */
68
74
  onOpenFullPage?: () => void;
75
+ /** The hotkey that opens the sheet, shown as keys in its header. */
76
+ shortcut?: HotkeyChord;
69
77
  };
70
- export declare function ChatSheet({ open, onOpenChange, title, context, placement, onPlacementChange, onOpenFullPage, ...conversationProps }: ChatSheetProps): import("react").JSX.Element | null;
78
+ export declare function ChatSheet({ open, onOpenChange, title, context, placement, onPlacementChange, onOpenFullPage, shortcut, ...conversationProps }: ChatSheetProps): import("react").JSX.Element | null;
71
79
  /**
72
80
  * The thread and the composer: everything that reads or writes the conversation. `layout="wide"`
73
81
  * sets both in one readable column for a full page; the sheet uses `sheet`.
74
82
  */
75
- export declare function ChatConversation({ source, placeholder, draft, onDraftChange, composerRef, mentions, mentionMark, onMentionSelect, mentionHref, events, assistantName: ownAssistantName, suggestions, copyReplies, variant, tasks, onTaskReply, layout, scrollBody: ScrollBody, }: ChatConversationProps & {
83
+ export declare function ChatConversation({ source, placeholder, draft, onDraftChange, composerRef, mentions, mentionMark, onMentionSelect, mentionHref, events, assistantName: ownAssistantName, suggestions, copyReplies, composerTools, dictation, variant, tasks, onTaskReply, layout, scrollBody: ScrollBody, }: ChatConversationProps & {
76
84
  layout?: "sheet" | "wide";
77
85
  /** The drawer's body inside a `Drawer`, a plain element otherwise. */
78
86
  scrollBody?: typeof DrawerBody | "div";
@@ -20,11 +20,12 @@ import { parseMentions } from "../chat/contract.js";
20
20
  import { MessageText, splitSources } from "../chat/message-text.js";
21
21
  import { AgentTaskCard, AgentWork } from "./agent-work.js";
22
22
  import { IconButton } from "./button.js";
23
- import { ChatComposer, ChatSuggestions, ChatSystemLine, ChatThread, } from "./chat.js";
23
+ import { ChatComposer, ChatComposerTool, ChatSuggestions, ChatSystemLine, ChatThread, } from "./chat.js";
24
24
  import { CopyButton } from "./copyable-id.js";
25
25
  import { Drawer, DrawerBody } from "./drawer.js";
26
26
  import { Dropzone, DropzoneFileList, useDropzonePicker } from "./dropzone.js";
27
27
  import { icon, Spinner } from "./icon.js";
28
+ import { ShortcutHint } from "./keycap.js";
28
29
  import { heading } from "./layout.js";
29
30
  import { useCiridaeLocale } from "./locale.js";
30
31
  export { useChatClientConversations, useChatClientSource, } from "./chat-client-source.js";
@@ -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
- const headerActions = css({ display: "flex", alignItems: "center", gap: "layout.tightGap" });
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,23 +147,27 @@ 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. */
@@ -178,10 +184,9 @@ const composerFooter = css({
178
184
  display: "grid",
179
185
  gap: "layout.tightGap",
180
186
  flexShrink: "0",
181
- paddingBlock: "layout.controlGap",
182
- paddingInline: "panel.default",
183
- borderTop: "1px solid token(colors.border.control)",
184
- "&[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" },
185
190
  });
186
191
  /* Over the composer, what goes with it: an error. */
187
192
  const footerStack = css({ display: "grid", gap: "layout.tightGap", minWidth: "0" });
@@ -189,19 +194,19 @@ const footerStack = css({ display: "grid", gap: "layout.tightGap", minWidth: "0"
189
194
  const wideColumn = css({ width: "100%", maxWidth: "chat.thread", marginInline: "auto" });
190
195
  /* In a drawer, the context row and the conversation stack under its header. */
191
196
  const drawerStack = css({ display: "flex", flex: "1", minHeight: "0", flexDirection: "column" });
192
- 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 }) {
193
198
  const { variant = "agent", tasks } = conversationProps;
194
199
  const { labels } = useCiridaeLocale();
195
200
  const asideRef = useRef(null);
196
201
  const dockedWidth = useDockedWidth(asideRef);
197
202
  const titleContent = (_jsxs("span", { className: titleLine, children: [_jsx(SparkleIcon, { className: icon({ size: "small" }), weight: "fill" }), title] }));
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 &&
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 &&
199
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 }) })))] }));
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 })] }));
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 })] }));
201
206
  if (placement === "docked") {
202
207
  if (!open)
203
208
  return null;
204
- 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 })] })] }));
205
210
  }
206
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 })] }) }));
207
212
  }
@@ -233,7 +238,7 @@ function useComposerFiles(upload, onUploadError) {
233
238
  * The thread and the composer: everything that reads or writes the conversation. `layout="wide"`
234
239
  * sets both in one readable column for a full page; the sheet uses `sheet`.
235
240
  */
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", }) {
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", }) {
237
242
  const { labels } = useCiridaeLocale();
238
243
  const assistantName = ownAssistantName ?? labels.chat.assistantName;
239
244
  const [ownDraft, setOwnDraft] = useState("");
@@ -273,13 +278,13 @@ export function ChatConversation({ source, placeholder, draft, onDraftChange, co
273
278
  });
274
279
  const wide = layout === "wide";
275
280
  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, {}) })] }) })] }));
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 })] }) })] }));
277
282
  }
278
283
  /** The paperclip: the enclosing `Dropzone`'s file picker. */
279
284
  function AttachFilesButton() {
280
285
  const { labels } = useCiridaeLocale();
281
286
  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" }) }) }));
287
+ return (_jsx(ChatComposerTool, { "aria-label": labels.chat.attachFile, disabled: picker.disabled, onClick: picker.openPicker, children: _jsx(PaperclipIcon, { className: icon({ size: "small" }) }) }));
283
288
  }
284
289
  /** Copies a reply's text, its mentions as their labels, and shows a tick for a moment. */
285
290
  function CopyReplyButton({ content }) {
@@ -289,7 +294,8 @@ function CopyReplyButton({ content }) {
289
294
  }
290
295
  /**
291
296
  * 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.
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.
293
299
  */
294
300
  function buildThread({ labels, source: { items, status, failureMessage, retry }, events, tasks, renderReply, replyActions, taskCard, }) {
295
301
  const entries = [];
@@ -300,10 +306,23 @@ function buildThread({ labels, source: { items, status, failureMessage, retry },
300
306
  latestTurnStart = index;
301
307
  });
302
308
  const latestTurnUnfinished = status === "running" || status === "failed";
303
- items.forEach((item, index) => {
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;
316
+ }
317
+ if (stepRun)
318
+ entries.push({ kind: "steps", ...stepRun });
319
+ stepRun = null;
304
320
  const inUnfinishedTurn = latestTurnUnfinished && index > latestTurnStart;
305
321
  entries.push(...toEntries(item, inUnfinishedTurn));
306
- });
322
+ }
323
+ const runningSteps = status === "running" ? stepRun : null;
324
+ if (stepRun && !runningSteps)
325
+ entries.push({ kind: "steps", ...stepRun });
307
326
  for (const event of events) {
308
327
  entries.push({
309
328
  kind: "line",
@@ -337,7 +356,16 @@ function buildThread({ labels, source: { items, status, failureMessage, retry },
337
356
  entries.sort((first, second) => first.at - second.at);
338
357
  const lastItem = items[items.length - 1];
339
358
  const at = Math.max(lastItem?.at ?? 0, entries[entries.length - 1]?.at ?? 0) || Date.now();
340
- appendTurnEnding(entries, { status, retry, failureMessage, lastItem, at, labels });
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
+ });
341
369
  return entries;
342
370
  function toEntries(item, inUnfinishedTurn) {
343
371
  switch (item.kind) {
@@ -353,13 +381,18 @@ function buildThread({ labels, source: { items, status, failureMessage, retry },
353
381
  attachments: item.attachments,
354
382
  sending: item.sending,
355
383
  sources: assistant
356
- ? splitSources(item.text).sources.map((source) => ({
357
- id: source.id,
358
- label: source.label,
359
- href: source.href,
360
- }))
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
+ ]
361
392
  : undefined,
362
393
  actions: assistant && !inUnfinishedTurn ? replyActions?.(item.text) : undefined,
394
+ suggestedActions: item.suggestedActions,
395
+ footnote: item.footnote,
363
396
  };
364
397
  if (!item.failure)
365
398
  return [message];
@@ -369,7 +402,8 @@ function buildThread({ labels, source: { items, status, failureMessage, retry },
369
402
  kind: "failure",
370
403
  id: `failure-${item.id}`,
371
404
  at: item.at,
372
- message: item.failure.message ?? labels.notSent,
405
+ title: labels.notSent,
406
+ message: item.failure.message,
373
407
  onRetry: item.failure.retry,
374
408
  },
375
409
  ];
@@ -381,14 +415,17 @@ function buildThread({ labels, source: { items, status, failureMessage, retry },
381
415
  }
382
416
  }
383
417
  }
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, }) {
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, }) {
386
423
  switch (status) {
387
424
  case "running":
388
- // Once its reply or a card waiting on the user is there, the agent is no longer typing.
425
+ // Once its reply or a card waiting on the user is there, the agent is no longer thinking.
389
426
  if (lastItem?.kind !== "card" &&
390
427
  !(lastItem?.kind === "message" && lastItem.role === "assistant"))
391
- entries.push({ kind: "typing", id: "typing", at });
428
+ entries.push({ kind: "typing", id: "typing", at, label: doing });
392
429
  return;
393
430
  case "failed": {
394
431
  const message = failureMessage ?? labels.failed;