agentic-ui-kit 0.3.0 → 0.5.0

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.
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  "use client";
2
- import { A as salvageJson, C as awaitingToolCalls, O as settleToolCall, S as applyEvent, b as toBlocks, d as isStateRef, f as matchesSchema, g as uiActionSchema, i as parseUiMarkup, v as readEventStream, x as activityTokens } from "./ui-markup-6xbP-myu.js";
2
+ import { C as toBlocks, E as awaitingToolCalls, N as salvageJson, T as applyEvent, _ as replacedId, d as isStateRef, f as matchesSchema, g as uiActionSchema, i as parseUiMarkup, j as settleToolCall, w as activityTokens, x as readEventStream } from "./ui-markup-CG9Lednh.js";
3
3
  import { clsx } from "clsx";
4
- import { Autocomplete, Button as Button$1, Dialog, Disclosure as Disclosure$1, DisclosurePanel, Focusable, Header, Heading, Input, Keyboard, Link, Menu as Menu$1, MenuItem as MenuItem$1, MenuSection as MenuSection$1, MenuTrigger, MenuTrigger as MenuTrigger$1, Modal as Modal$1, ModalOverlay, Popover as Popover$1, SearchField, Separator, SubmenuTrigger, Text, Tooltip as Tooltip$1, TooltipTrigger, useFilter } from "react-aria-components";
4
+ import { Autocomplete, Button as Button$1, Dialog, DialogTrigger, Disclosure as Disclosure$1, DisclosurePanel, Focusable, Header, Heading, Input, Keyboard, Link, Menu as Menu$1, MenuItem as MenuItem$1, MenuSection as MenuSection$1, MenuTrigger, MenuTrigger as MenuTrigger$1, Modal as Modal$1, ModalOverlay, Popover as Popover$1, SearchField, Separator, SubmenuTrigger, Text, ToggleButton, ToggleButtonGroup, Tooltip as Tooltip$1, TooltipTrigger, useFilter } from "react-aria-components";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ import { Component, Fragment as Fragment$1, createContext, createElement, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
6
7
  import { AlertTriangle, ArrowDown, ArrowUp, Check, ChevronDown, ChevronRight, ChevronsUpDown, Copy, FileText, Folder, FolderOpen, FolderPlus, Globe, Info, MoreHorizontal, PanelLeft, Pencil, Pin, PinOff, Plus, RefreshCw, Share, ShieldQuestion, Square, ThumbsDown, ThumbsUp, Trash2, Volume2, X } from "lucide-react";
7
- import { Component, Fragment as Fragment$1, createContext, createElement, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
8
8
  import { UNSAFE_PortalProvider } from "react-aria";
9
9
  import { createMathPlugin } from "@streamdown/math";
10
10
  import { Streamdown, defaultRehypePlugins } from "streamdown";
@@ -27,25 +27,34 @@ const variants = {
27
27
  /** Plain text button, highlights on hover. Sidebar rows, header buttons. */
28
28
  ghost: "text-chat-fg data-hovered:bg-chat-hover data-pressed:bg-chat-selected",
29
29
  /** Filled with the surface color, e.g. the selected Chat/Cowork toggle. */
30
- subtle: "bg-chat-surface text-chat-fg data-hovered:bg-chat-hover data-pressed:bg-chat-selected",
31
- /** The send button. */
32
- primary: "bg-chat-accent text-chat-accent-fg data-hovered:brightness-110 data-pressed:brightness-95"
30
+ subtle: "bg-chat-surface font-medium text-chat-fg data-hovered:bg-chat-hover data-pressed:bg-chat-selected",
31
+ /** A thin border, no fill: secondary actions next to a primary one, e.g. Cancel or Share. */
32
+ outline: "border border-chat-border-strong font-medium text-chat-fg data-hovered:bg-chat-hover data-pressed:bg-chat-selected",
33
+ /** The send button and the one main action of a dialog. */
34
+ primary: "bg-chat-accent font-medium text-chat-accent-fg data-hovered:brightness-110 data-pressed:brightness-95",
35
+ /**
36
+ * Destructive confirmations, e.g. Delete chat. Fill --chat-danger-bg (default: --chat-danger, darkened so it
37
+ * doesn't read as the accent), text --chat-danger-fg (default white).
38
+ */
39
+ danger: "bg-[var(--chat-danger-bg,color-mix(in_srgb,var(--chat-danger)_70%,black))] font-medium text-[var(--chat-danger-fg,#fff)] data-hovered:brightness-110 data-pressed:brightness-95"
33
40
  };
34
41
  const sizes = {
35
42
  sm: "h-7 gap-1.5 px-2 text-(length:--chat-ui-font-size) leading-5 [@media(pointer:coarse)]:h-9",
36
- md: "h-8 gap-2 px-2.5 text-(length:--chat-ui-font-size) leading-5 [@media(pointer:coarse)]:h-10"
43
+ md: "h-8 gap-2 px-2.5 text-(length:--chat-ui-font-size) leading-5 [@media(pointer:coarse)]:h-10",
44
+ /** Dialog and empty-state actions. */
45
+ lg: "h-9 gap-2 px-3.5 text-(length:--chat-ui-font-size) leading-5 [@media(pointer:coarse)]:h-11"
37
46
  };
38
47
  function Button({ variant = "ghost", size = "md", className, ...props }) {
39
48
  return /* @__PURE__ */ jsx(Button$1, {
40
49
  ...props,
41
- className: clsx("inline-flex cursor-pointer items-center rounded-chat-sm font-chat-sans whitespace-nowrap transition-colors select-none", "data-disabled:cursor-default data-disabled:opacity-40", focusRing, variants[variant], sizes[size], className)
50
+ className: clsx("inline-flex cursor-pointer items-center rounded-chat-sm font-chat-sans whitespace-nowrap transition-colors select-none [&_svg]:size-4 [&_svg]:shrink-0", "data-disabled:cursor-default data-disabled:opacity-40", focusRing, variants[variant], sizes[size], className)
42
51
  });
43
52
  }
44
53
  function IconButton({ label, variant = "ghost", size = "md", tooltip = true, className, ...props }) {
45
54
  const button = /* @__PURE__ */ jsx(Button$1, {
46
55
  ...props,
47
56
  "aria-label": label,
48
- className: clsx("inline-flex shrink-0 cursor-pointer items-center justify-center rounded-chat-sm text-chat-muted transition-colors select-none", "data-hovered:text-chat-fg data-disabled:cursor-default data-disabled:opacity-40 [&_svg]:size-4", focusRing, variants[variant], variant === "ghost" && "text-chat-muted", size === "sm" ? "size-7 [@media(pointer:coarse)]:size-9" : "size-8 [@media(pointer:coarse)]:size-10", className)
57
+ className: clsx("inline-flex shrink-0 cursor-pointer items-center justify-center rounded-chat-sm transition-colors select-none", "data-disabled:cursor-default data-disabled:opacity-40 [&_svg]:size-4", focusRing, variants[variant], variant === "ghost" && "text-chat-muted data-hovered:text-chat-fg", size === "sm" ? "size-7 [@media(pointer:coarse)]:size-9" : "size-8 [@media(pointer:coarse)]:size-10", className)
49
58
  });
50
59
  return tooltip ? /* @__PURE__ */ jsx(Tooltip, {
51
60
  content: label,
@@ -142,6 +151,176 @@ function IconSlot({ children, className }) {
142
151
  children
143
152
  });
144
153
  }
154
+ // src/primitives/labels.tsx
155
+ const plural = (count, one, many) => count === 1 ? one : many(count);
156
+ /** The English strings. */
157
+ const defaultLabels = {
158
+ thinking: "Thinking…",
159
+ thought: "Thought",
160
+ thoughtFor: (duration) => `Thought for ${duration}`,
161
+ waitingForYou: " · waiting for you",
162
+ usedTool: (name) => `used ${name}`,
163
+ joinSummaries: (phrases) => {
164
+ const text = phrases.join(", ");
165
+ return text.charAt(0).toUpperCase() + text.slice(1);
166
+ },
167
+ duration: (seconds) => seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`,
168
+ ranCommandLabel: "Ran a command",
169
+ ranCommands: (count) => plural(count, "ran a command", (n) => `ran ${n} commands`),
170
+ searchedWebLabel: "Searched the web",
171
+ readPageLabel: "Read page",
172
+ readPages: (count) => plural(count, "read a page", (n) => `read ${n} pages`),
173
+ searchedWeb: (count) => plural(count, "searched the web", (n) => `searched the web ${n} times`),
174
+ recalledMemoryLabel: "Recalled memory",
175
+ recalledMemory: () => "recalled memory",
176
+ output: "Output",
177
+ noOutput: "(no output)",
178
+ noResults: "No results",
179
+ reasoningHidden: "The model's reasoning is not shown.",
180
+ allowTool: (name) => `Allow ${name}?`,
181
+ allowAction: (label) => /* @__PURE__ */ jsxs(Fragment, { children: [
182
+ "Allow ",
183
+ label,
184
+ "?"
185
+ ] }),
186
+ allow: "Allow",
187
+ deny: "Deny",
188
+ sidebar: "Sidebar",
189
+ closeSidebar: "Close sidebar",
190
+ openSidebar: "Open sidebar",
191
+ expandSidebar: "Expand sidebar",
192
+ collapseSidebar: "Collapse sidebar",
193
+ context: "Context",
194
+ contextUsed: "Context used",
195
+ percentUsed: (percent) => `${percent}% used`,
196
+ freeSpace: "Free space",
197
+ tokenCount: (count) => `${count} tokens`,
198
+ replyPlaceholder: "Reply…",
199
+ replyDirectly: "Or reply directly…",
200
+ message: "Message",
201
+ stop: "Stop",
202
+ send: "Send",
203
+ addFiles: "Add files and more",
204
+ attachments: "Attachments",
205
+ editPastedText: "Edit pasted text",
206
+ viewPastedText: "View pasted text",
207
+ removeAttachment: (name) => `Remove ${name}`,
208
+ pastedText: "Pasted text",
209
+ pasted: "Pasted",
210
+ failed: "Failed",
211
+ file: "File",
212
+ uploadFailed: "Upload failed",
213
+ cancel: "Cancel",
214
+ close: "Close",
215
+ save: "Save",
216
+ chats: "Chats",
217
+ running: "Running",
218
+ unread: "Unread",
219
+ moreOptionsFor: (title) => `More options for ${title}`,
220
+ new: "New",
221
+ viewAll: "View all",
222
+ chatActions: "Chat actions",
223
+ pin: "Pin",
224
+ unpin: "Unpin",
225
+ rename: "Rename",
226
+ addToProject: "Add to project",
227
+ searchProjects: "Search or create a project",
228
+ projects: "Projects",
229
+ newProject: "New project",
230
+ shareChat: "Share chat",
231
+ delete: "Delete",
232
+ justNow: "just now",
233
+ copy: "Copy",
234
+ copied: "Copied",
235
+ retry: "Retry",
236
+ edit: "Edit",
237
+ readAloud: "Read aloud",
238
+ goodResponse: "Good response",
239
+ badResponse: "Bad response",
240
+ somethingWentWrong: "Something went wrong.",
241
+ conversation: "Conversation",
242
+ scrollToBottom: "Scroll to bottom",
243
+ showMore: "Show more",
244
+ showLess: "Show less",
245
+ model: "Model",
246
+ modelButton: (label) => `Model: ${label}`,
247
+ effort: "Effort",
248
+ effortNote: "Higher effort means more thorough responses, but takes longer and uses your limits faster.",
249
+ recommended: "Recommended",
250
+ effortFaster: "Faster",
251
+ effortSmarter: "Smarter",
252
+ moreModels: "More models",
253
+ widget: "Widget",
254
+ visualUpdated: "Updated below ↓",
255
+ visualUpdating: "Updating…",
256
+ drawingWidget: "Drawing"
257
+ };
258
+ const LabelsContext = createContext(defaultLabels);
259
+ /** Merges `labels` over the labels of the enclosing provider (or the defaults). */
260
+ function LabelsProvider({ labels, children }) {
261
+ const parent = useLabels();
262
+ const merged = useMemo(() => labels ? {
263
+ ...parent,
264
+ ...labels
265
+ } : parent, [parent, labels]);
266
+ return /* @__PURE__ */ jsx(LabelsContext.Provider, {
267
+ value: merged,
268
+ children
269
+ });
270
+ }
271
+ /** The active UI strings: the enclosing `ChatTheme`'s `labels` over the English defaults. */
272
+ function useLabels() {
273
+ return useContext(LabelsContext);
274
+ }
275
+ // src/primitives/copy-button.tsx
276
+ /** Copies `text`; shows a check for a moment after. */
277
+ function CopyButton({ text, className }) {
278
+ const icons = useIcons();
279
+ const labels = useLabels();
280
+ const [copied, setCopied] = useState(false);
281
+ return /* @__PURE__ */ jsx(IconButton, {
282
+ label: copied ? labels.copied : labels.copy,
283
+ size: "sm",
284
+ ...className && { className },
285
+ onPress: () => {
286
+ navigator.clipboard?.writeText(text);
287
+ setCopied(true);
288
+ setTimeout(() => setCopied(false), 1500);
289
+ },
290
+ children: copied ? icons.copied : icons.copy
291
+ });
292
+ }
293
+ // src/primitives/segmented-control.tsx
294
+ /**
295
+ * One of a few options, side by side on a track, like claude.ai's Chat / Cowork switch. Themed with
296
+ * `--chat-segment-track` (default: a shade darker than the surface it sits on) and `--chat-segment-selected`
297
+ * (default `--chat-raised`).
298
+ */
299
+ function SegmentedControl({ label, options, value, onChange, className }) {
300
+ return /* @__PURE__ */ jsx(ToggleButtonGroup, {
301
+ "aria-label": label,
302
+ selectionMode: "single",
303
+ disallowEmptySelection: true,
304
+ selectedKeys: [value],
305
+ onSelectionChange: (keys) => {
306
+ const next = [...keys][0];
307
+ if (next !== void 0) onChange(next);
308
+ },
309
+ className: clsx("inline-flex shrink-0 items-center gap-0.5 rounded-chat-md p-0.5", "bg-[var(--chat-segment-track,color-mix(in_srgb,var(--chat-bg)_55%,transparent))]", className),
310
+ children: options.map((o) => {
311
+ const button = /* @__PURE__ */ jsxs(ToggleButton, {
312
+ id: o.id,
313
+ ...o.iconOnly && { "aria-label": o.label },
314
+ className: clsx("inline-flex h-6 cursor-pointer items-center justify-center gap-1.5 rounded-[calc(var(--chat-radius-md)-2px)] font-chat-sans text-(length:--chat-ui-font-size) leading-5 text-chat-muted outline-none transition-colors select-none [&_svg]:size-4", o.iconOnly ? "w-7" : "px-2", "data-hovered:text-chat-fg data-focus-visible:ring-2 data-focus-visible:ring-chat-focus", "data-selected:bg-[var(--chat-segment-selected,var(--chat-raised))] data-selected:text-chat-fg data-selected:shadow-[inset_0_0_0_1px_var(--chat-border-strong)]"),
315
+ children: [o.icon, !o.iconOnly && o.label]
316
+ }, o.id);
317
+ return o.iconOnly ? /* @__PURE__ */ jsx(Tooltip, {
318
+ content: o.label,
319
+ children: button
320
+ }, o.id) : button;
321
+ })
322
+ });
323
+ }
145
324
  // src/primitives/disclosure.tsx
146
325
  /** A row that expands to show more, like "Recalled memory ▸". */
147
326
  function Disclosure({ summary, children, className, summaryClassName, panelClassName, indicator = "caret", trailing, ...props }) {
@@ -212,7 +391,7 @@ function Menu({ className, ...props }) {
212
391
  className: clsx("max-h-[inherit] min-w-52 overflow-auto p-1.5 outline-none", className)
213
392
  });
214
393
  }
215
- function MenuItem({ children, icon, description, detail, shortcut, tone = "default", className, ...props }) {
394
+ function MenuItem({ children, icon, description, descriptionPlacement = "below", detail, shortcut, tone = "default", className, ...props }) {
216
395
  const icons = useIcons();
217
396
  const textValue = props.textValue ?? (typeof children === "string" ? children : void 0);
218
397
  return /* @__PURE__ */ jsx(MenuItem$1, {
@@ -225,14 +404,14 @@ function MenuItem({ children, icon, description, detail, shortcut, tone = "defau
225
404
  children: icon
226
405
  }),
227
406
  /* @__PURE__ */ jsxs("span", {
228
- className: "flex min-w-0 flex-1 flex-col",
407
+ className: clsx("flex min-w-0 flex-1", descriptionPlacement === "beside" ? "items-baseline gap-2" : "flex-col"),
229
408
  children: [/* @__PURE__ */ jsx(Text, {
230
409
  slot: "label",
231
- className: "truncate",
410
+ className: clsx("truncate", descriptionPlacement === "beside" && "shrink-0"),
232
411
  children
233
412
  }), description && /* @__PURE__ */ jsx(Text, {
234
413
  slot: "description",
235
- className: "text-(length:--chat-ui-font-size-sm) leading-4 text-chat-muted",
414
+ className: clsx("text-chat-muted", descriptionPlacement === "beside" ? "truncate text-(length:--chat-ui-font-size) leading-5" : "text-(length:--chat-ui-font-size-sm) leading-4"),
236
415
  children: description
237
416
  })]
238
417
  }),
@@ -519,7 +698,7 @@ function useHighlight(code, language) {
519
698
  }
520
699
  // src/primitives/code-block.tsx
521
700
  /** Monospace code with Shiki highlighting, scrolling past a fixed height. */
522
- function CodeBlock({ code, language, label = language, variant = "plain", className }) {
701
+ function CodeBlock({ code, language, label = language, variant = "plain", copy = variant === "boxed", className }) {
523
702
  const result = useHighlight(code, language);
524
703
  const lines = result?.lines;
525
704
  const themeBackground = useCodeThemes().background === "theme" && variant === "boxed" && !!result;
@@ -531,9 +710,12 @@ function CodeBlock({ code, language, label = language, variant = "plain", classN
531
710
  "data-chat-code-bg": themeBackground ? "theme" : void 0,
532
711
  style: themeBackground ? result.vars : void 0,
533
712
  className: clsx(variant === "boxed" && "my-4 rounded-chat-md border border-chat-code-border bg-chat-code-bg px-4 py-3.5", className),
534
- children: [label && /* @__PURE__ */ jsx("div", {
535
- className: "mb-3 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle",
536
- children: label
713
+ children: [(label || copy) && /* @__PURE__ */ jsxs("div", {
714
+ className: clsx("flex items-center gap-2 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle", copy ? "-mt-1.5 -mr-2 mb-1.5" : "mb-3"),
715
+ children: [/* @__PURE__ */ jsx("span", {
716
+ className: "min-w-0 flex-1 truncate",
717
+ children: label
718
+ }), copy && /* @__PURE__ */ jsx(CopyButton, { text: code })]
537
719
  }), /* @__PURE__ */ jsx("pre", {
538
720
  className: clsx("overflow-auto font-chat-mono text-(length:--chat-code-font-size) leading-[1.75] whitespace-pre text-chat-fg", variant === "plain" && "max-h-56", "[scrollbar-color:var(--chat-border-strong)_transparent] [scrollbar-width:thin]"),
539
721
  children: /* @__PURE__ */ jsxs("code", { children: [highlighted ? highlighted.map((line, i) => /* @__PURE__ */ jsxs(Fragment$1, { children: [i > 0 && "\n", line.map((token, j) => /* @__PURE__ */ jsx("span", {
@@ -544,123 +726,6 @@ function CodeBlock({ code, language, label = language, variant = "plain", classN
544
726
  })]
545
727
  });
546
728
  }
547
- // src/primitives/labels.tsx
548
- const plural = (count, one, many) => count === 1 ? one : many(count);
549
- /** The English strings. */
550
- const defaultLabels = {
551
- thinking: "Thinking…",
552
- thought: "Thought",
553
- thoughtFor: (duration) => `Thought for ${duration}`,
554
- waitingForYou: " · waiting for you",
555
- usedTool: (name) => `used ${name}`,
556
- joinSummaries: (phrases) => {
557
- const text = phrases.join(", ");
558
- return text.charAt(0).toUpperCase() + text.slice(1);
559
- },
560
- duration: (seconds) => seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`,
561
- ranCommandLabel: "Ran a command",
562
- ranCommands: (count) => plural(count, "ran a command", (n) => `ran ${n} commands`),
563
- searchedWebLabel: "Searched the web",
564
- readPageLabel: "Read page",
565
- readPages: (count) => plural(count, "read a page", (n) => `read ${n} pages`),
566
- searchedWeb: (count) => plural(count, "searched the web", (n) => `searched the web ${n} times`),
567
- recalledMemoryLabel: "Recalled memory",
568
- recalledMemory: () => "recalled memory",
569
- output: "Output",
570
- noOutput: "(no output)",
571
- noResults: "No results",
572
- reasoningHidden: "The model's reasoning is not shown.",
573
- allowTool: (name) => `Allow ${name}?`,
574
- allowAction: (label) => /* @__PURE__ */ jsxs(Fragment, { children: [
575
- "Allow ",
576
- label,
577
- "?"
578
- ] }),
579
- allow: "Allow",
580
- deny: "Deny",
581
- sidebar: "Sidebar",
582
- closeSidebar: "Close sidebar",
583
- openSidebar: "Open sidebar",
584
- expandSidebar: "Expand sidebar",
585
- collapseSidebar: "Collapse sidebar",
586
- context: "Context",
587
- contextUsed: "Context used",
588
- percentUsed: (percent) => `${percent}% used`,
589
- freeSpace: "Free space",
590
- tokenCount: (count) => `${count} tokens`,
591
- replyPlaceholder: "Reply…",
592
- replyDirectly: "Or reply directly…",
593
- message: "Message",
594
- stop: "Stop",
595
- send: "Send",
596
- addFiles: "Add files and more",
597
- attachments: "Attachments",
598
- editPastedText: "Edit pasted text",
599
- viewPastedText: "View pasted text",
600
- removeAttachment: (name) => `Remove ${name}`,
601
- pastedText: "Pasted text",
602
- pasted: "Pasted",
603
- failed: "Failed",
604
- file: "File",
605
- uploadFailed: "Upload failed",
606
- cancel: "Cancel",
607
- close: "Close",
608
- save: "Save",
609
- chats: "Chats",
610
- running: "Running",
611
- unread: "Unread",
612
- moreOptionsFor: (title) => `More options for ${title}`,
613
- new: "New",
614
- viewAll: "View all",
615
- chatActions: "Chat actions",
616
- pin: "Pin",
617
- unpin: "Unpin",
618
- rename: "Rename",
619
- addToProject: "Add to project",
620
- searchProjects: "Search or create a project",
621
- projects: "Projects",
622
- newProject: "New project",
623
- shareChat: "Share chat",
624
- delete: "Delete",
625
- justNow: "just now",
626
- copy: "Copy",
627
- copied: "Copied",
628
- retry: "Retry",
629
- edit: "Edit",
630
- readAloud: "Read aloud",
631
- goodResponse: "Good response",
632
- badResponse: "Bad response",
633
- somethingWentWrong: "Something went wrong.",
634
- conversation: "Conversation",
635
- scrollToBottom: "Scroll to bottom",
636
- showMore: "Show more",
637
- showLess: "Show less",
638
- model: "Model",
639
- modelButton: (label) => `Model: ${label}`,
640
- effort: "Effort",
641
- effortNote: "Higher effort means more thorough responses, but takes longer and uses your limits faster.",
642
- recommended: "Recommended",
643
- moreModels: "More models",
644
- widget: "Widget",
645
- drawingWidget: "Drawing"
646
- };
647
- const LabelsContext = createContext(defaultLabels);
648
- /** Merges `labels` over the labels of the enclosing provider (or the defaults). */
649
- function LabelsProvider({ labels, children }) {
650
- const parent = useLabels();
651
- const merged = useMemo(() => labels ? {
652
- ...parent,
653
- ...labels
654
- } : parent, [parent, labels]);
655
- return /* @__PURE__ */ jsx(LabelsContext.Provider, {
656
- value: merged,
657
- children
658
- });
659
- }
660
- /** The active UI strings: the enclosing `ChatTheme`'s `labels` over the English defaults. */
661
- function useLabels() {
662
- return useContext(LabelsContext);
663
- }
664
729
  // src/primitives/chat-theme.tsx
665
730
  /**
666
731
  * Applies a chat theme to part of a page. Menus, tooltips and dialogs opened inside render into this
@@ -1207,6 +1272,87 @@ const SidebarContext = createContext({
1207
1272
  dismiss: () => {}
1208
1273
  });
1209
1274
  const useSidebar = () => useContext(SidebarContext);
1275
+ // src/components/replacements.tsx
1276
+ /**
1277
+ * In-place updates of drawn visuals: a tool call whose input has `replaces: <id>` takes the place of that
1278
+ * earlier call. In the same reply the newest finished call is drawn where the first one was, and the old one
1279
+ * stays (dimmed) until the new one's input is complete. A call replaced from a later reply folds into a link.
1280
+ */
1281
+ /** Ready to take the old one's place: its input is complete and it didn't fail. */
1282
+ const isReady = (p) => p.status !== "streaming" && p.status !== "input" && p.status !== "error";
1283
+ const callsOf = (m) => m.parts.filter((p) => p.type === "tool_call");
1284
+ const replaces = (p) => replacedId(p.input ?? p.partialInput);
1285
+ /** One reply's chains: each call's first call (where the chain is drawn) and each first call's chain, in order. */
1286
+ function chainsOf(message) {
1287
+ const first = /* @__PURE__ */ new Map();
1288
+ const chains = /* @__PURE__ */ new Map();
1289
+ for (const call of callsOf(message)) {
1290
+ const r = replaces(call);
1291
+ const root = r !== void 0 && first.has(r) ? first.get(r) : call.id;
1292
+ first.set(call.id, root);
1293
+ chains.set(root, [...chains.get(root) ?? [], call]);
1294
+ }
1295
+ return {
1296
+ first,
1297
+ chains
1298
+ };
1299
+ }
1300
+ /** Calls replaced by a finished call in a later reply: replaced id → the newest such call. */
1301
+ function laterReplacements(messages) {
1302
+ const later = /* @__PURE__ */ new Map();
1303
+ for (const m of messages) {
1304
+ if (m.role !== "assistant") continue;
1305
+ const own = new Set(callsOf(m).map((c) => c.id));
1306
+ for (const call of callsOf(m)) {
1307
+ const r = replaces(call);
1308
+ if (r !== void 0 && !own.has(r) && isReady(call)) later.set(r, call);
1309
+ }
1310
+ }
1311
+ return later;
1312
+ }
1313
+ const LaterContext = createContext(/* @__PURE__ */ new Map());
1314
+ function LaterReplacements({ value, children }) {
1315
+ return /* @__PURE__ */ jsx(LaterContext.Provider, {
1316
+ value,
1317
+ children
1318
+ });
1319
+ }
1320
+ /** The call in a later reply that replaces any call of this chain, if there is one. */
1321
+ function useLaterReplacement(chain) {
1322
+ const later = useContext(LaterContext);
1323
+ for (let i = chain.length - 1; i >= 0; i--) {
1324
+ const by = later.get(chain[i].id);
1325
+ if (by) return by;
1326
+ }
1327
+ }
1328
+ /** The DOM id of a drawn tool call, for the "Updated below" link. */
1329
+ const toolAnchor = (id) => `chat-tool-${id}`;
1330
+ /** Draws a chain: the newest finished call (dimmed while a newer one streams), or a link to its later update. */
1331
+ function ToolChain({ chain, draw }) {
1332
+ const labels = useLabels();
1333
+ const later = useLaterReplacement(chain);
1334
+ const shown = chain.findLast(isReady) ?? chain[0];
1335
+ const updating = chain.indexOf(shown) < chain.length - 1 && chain.at(-1).status !== "error";
1336
+ if (later) return /* @__PURE__ */ jsx(Button, {
1337
+ variant: "ghost",
1338
+ size: "sm",
1339
+ className: "self-start text-chat-muted data-hovered:text-chat-fg",
1340
+ onPress: () => document.getElementById(toolAnchor(later.id))?.scrollIntoView({
1341
+ behavior: "smooth",
1342
+ block: "center"
1343
+ }),
1344
+ children: labels.visualUpdated
1345
+ });
1346
+ return /* @__PURE__ */ jsxs("div", {
1347
+ id: toolAnchor(chain[0].id),
1348
+ "aria-busy": updating || void 0,
1349
+ className: updating ? "opacity-60 transition-opacity" : "transition-opacity",
1350
+ children: [/* @__PURE__ */ jsx("div", { children: draw(shown) }, shown.id), updating && /* @__PURE__ */ jsx("p", {
1351
+ className: "chat-shimmer mt-1 font-chat-sans text-(length:--chat-ui-font-size-sm) text-chat-muted",
1352
+ children: labels.visualUpdating
1353
+ })]
1354
+ });
1355
+ }
1210
1356
  // src/components/widget-frame.tsx
1211
1357
  const WidgetHostContext = createContext({});
1212
1358
  /** Sets what widgets below may do. ChatView and ChatTranscript set it from their `widgets` prop. */
@@ -1322,6 +1468,8 @@ const BOOT = `(() => {
1322
1468
  new ResizeObserver(() => {
1323
1469
  const h = Math.ceil(root.getBoundingClientRect().height);
1324
1470
  if (h !== last) { last = h; post("ui/notifications/size-changed", { height: h }); }
1471
+ // The frame grows to fit, a moment behind; only content past the cap gets a scrollbar.
1472
+ document.documentElement.style.overflowY = h > ${WIDGET_MAX_HEIGHT} ? "auto" : "hidden";
1325
1473
  }).observe(root);
1326
1474
  const seen = new Set();
1327
1475
  const report = (text) => { if (!seen.has(text) && seen.size < 10) { seen.add(text); post("notifications/message", { level: "error", logger: "widget", data: text }); } };
@@ -1334,6 +1482,7 @@ function fragmentDoc(csp, theme) {
1334
1482
  <meta http-equiv="Content-Security-Policy" content="${csp}">
1335
1483
  <style>
1336
1484
  :root { ${Object.entries(theme.vars).map(([k, v]) => `${k}: ${v};`).join(" ")} color-scheme: ${theme.scheme}; }
1485
+ html { overflow: hidden; }
1337
1486
  html, body { margin: 0; background: transparent; color: var(--chat-fg); font-family: var(--chat-font-sans);
1338
1487
  font-size: var(--chat-ui-font-size); line-height: 1.5; }
1339
1488
  #widget { display: flow-root; }
@@ -2348,10 +2497,14 @@ function ActivityGroup({ parts, defaultExpanded = false, className }) {
2348
2497
  }) : /* @__PURE__ */ jsx(Disclosure, {
2349
2498
  isExpanded: expanded,
2350
2499
  onExpandedChange: setExpanded,
2351
- summary: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Live, {
2352
- live,
2353
- children: activityHeading(parts, views, labels)
2354
- }), after] }),
2500
+ summary: /* @__PURE__ */ jsxs(Fragment, { children: [
2501
+ /* @__PURE__ */ jsx(Lead, { live }),
2502
+ /* @__PURE__ */ jsx(Live, {
2503
+ live,
2504
+ children: activityHeading(parts, views, labels)
2505
+ }),
2506
+ after
2507
+ ] }),
2355
2508
  summaryClassName: "text-(length:--chat-activity-font-size)",
2356
2509
  panelClassName: "pt-3",
2357
2510
  children: /* @__PURE__ */ jsx("div", {
@@ -2361,6 +2514,17 @@ function ActivityGroup({ parts, defaultExpanded = false, className }) {
2361
2514
  })
2362
2515
  });
2363
2516
  }
2517
+ /** The host's mark left of the summary (`icons.activity` while working, `icons.activityDone` after), if set. */
2518
+ function Lead({ live }) {
2519
+ const icons = useIcons();
2520
+ const icon = live ? icons.activity : icons.activityDone;
2521
+ if (!icon) return null;
2522
+ return /* @__PURE__ */ jsx("span", {
2523
+ "aria-hidden": true,
2524
+ className: "chat-activity-icon mr-1.5 inline-flex size-[1.15em] shrink-0 items-center self-center [&_svg]:size-full",
2525
+ children: icon
2526
+ });
2527
+ }
2364
2528
  const SHIMMER_MS = 2e3;
2365
2529
  /**
2366
2530
  * Sweeps a highlight across a label while its step is running. The animation starts at the phase of a
@@ -2385,10 +2549,14 @@ function SingleStep({ part, live, after, expanded, onExpandedChange }) {
2385
2549
  return /* @__PURE__ */ jsx(Disclosure, {
2386
2550
  isExpanded: expanded,
2387
2551
  onExpandedChange,
2388
- summary: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Live, {
2389
- live,
2390
- children: summary
2391
- }), after] }),
2552
+ summary: /* @__PURE__ */ jsxs(Fragment, { children: [
2553
+ /* @__PURE__ */ jsx(Lead, { live }),
2554
+ /* @__PURE__ */ jsx(Live, {
2555
+ live,
2556
+ children: summary
2557
+ }),
2558
+ after
2559
+ ] }),
2392
2560
  summaryClassName: "text-(length:--chat-activity-font-size)",
2393
2561
  panelClassName: "pt-3",
2394
2562
  children: /* @__PURE__ */ jsx("div", {
@@ -2689,24 +2857,9 @@ function formatRelativeTime(iso, now = Date.now(), labels = defaultLabels) {
2689
2857
  function messageText(message) {
2690
2858
  return message.parts.map((p) => p.type === "text" ? p.text : p.type === "custom" ? p.text ?? "" : "").filter(Boolean).join("\n\n");
2691
2859
  }
2692
- function CopyButton({ text }) {
2693
- const icons = useIcons();
2694
- const labels = useLabels();
2695
- const [copied, setCopied] = useState(false);
2696
- return /* @__PURE__ */ jsx(IconButton, {
2697
- label: copied ? labels.copied : labels.copy,
2698
- size: "sm",
2699
- onPress: () => {
2700
- navigator.clipboard?.writeText(text);
2701
- setCopied(true);
2702
- setTimeout(() => setCopied(false), 1500);
2703
- },
2704
- children: copied ? icons.copied : icons.copy
2705
- });
2706
- }
2707
2860
  const timeClass = "px-1 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle";
2708
2861
  /** The user's turn: a bubble on the right, with time, retry, edit and copy under it on hover. */
2709
- function UserMessage({ message, onRetry, onEdit, actions, className }) {
2862
+ function UserMessage({ message, onRetry, onEdit, actions, copy = true, time = true, className }) {
2710
2863
  const icons = useIcons();
2711
2864
  const labels = useLabels();
2712
2865
  const text = messageText(message);
@@ -2732,7 +2885,7 @@ function UserMessage({ message, onRetry, onEdit, actions, className }) {
2732
2885
  /* @__PURE__ */ jsxs("div", {
2733
2886
  className: "mt-1 flex items-center gap-0.5 opacity-0 transition-opacity group-hover/message:opacity-100 group-focus-within/message:opacity-100 [@media(hover:none)]:opacity-100",
2734
2887
  children: [
2735
- /* @__PURE__ */ jsx("span", {
2888
+ time && /* @__PURE__ */ jsx("span", {
2736
2889
  className: timeClass,
2737
2890
  children: formatRelativeTime(message.createdAt, void 0, labels)
2738
2891
  }),
@@ -2748,7 +2901,7 @@ function UserMessage({ message, onRetry, onEdit, actions, className }) {
2748
2901
  onPress: onEdit,
2749
2902
  children: icons.edit
2750
2903
  }),
2751
- /* @__PURE__ */ jsx(CopyButton, { text }),
2904
+ copy && /* @__PURE__ */ jsx(CopyButton, { text }),
2752
2905
  actions
2753
2906
  ]
2754
2907
  })
@@ -2792,12 +2945,13 @@ function UserBubble({ text }) {
2792
2945
  * The assistant's turn: activity groups (each with its own token count), markdown, and other parts in
2793
2946
  * order. When done, copy / read aloud / feedback / retry and the time sit under it.
2794
2947
  */
2795
- function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPart, actions, className }) {
2948
+ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPart, actions, copy = true, time = true, className }) {
2796
2949
  const icons = useIcons();
2797
2950
  const labels = useLabels();
2798
2951
  const streaming = message.status === "streaming";
2799
2952
  const views = useToolViews();
2800
2953
  const blocks = toBlocks(message.parts, (part) => views[part.name]?.Block !== void 0);
2954
+ const { first, chains } = chainsOf(message);
2801
2955
  return /* @__PURE__ */ jsxs("div", {
2802
2956
  className: clsx("group/message flex flex-col gap-4", className),
2803
2957
  children: [
@@ -2812,8 +2966,14 @@ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPar
2812
2966
  })] }, block.id);
2813
2967
  }
2814
2968
  if (block.kind === "tool") {
2815
- const ToolBlock = views[block.part.name].Block;
2816
- return /* @__PURE__ */ jsx(ToolBlock, { part: block.part }, block.id);
2969
+ if (first.get(block.part.id) !== block.part.id) return null;
2970
+ return /* @__PURE__ */ jsx(ToolChain, {
2971
+ chain: chains.get(block.part.id) ?? [block.part],
2972
+ draw: (part) => {
2973
+ const ToolBlock = (views[part.name] ?? views[block.part.name]).Block;
2974
+ return /* @__PURE__ */ jsx(ToolBlock, { part });
2975
+ }
2976
+ }, block.id);
2817
2977
  }
2818
2978
  const part = block.part;
2819
2979
  if (part.type === "text") return /* @__PURE__ */ jsx(Markdown, {
@@ -2834,7 +2994,7 @@ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPar
2834
2994
  streaming ? /* @__PURE__ */ jsx("div", { className: "h-7" }) : /* @__PURE__ */ jsxs("div", {
2835
2995
  className: "-ml-1.5 flex h-7 items-center gap-0.5",
2836
2996
  children: [
2837
- /* @__PURE__ */ jsx(CopyButton, { text: messageText(message) }),
2997
+ copy && /* @__PURE__ */ jsx(CopyButton, { text: messageText(message) }),
2838
2998
  onReadAloud && /* @__PURE__ */ jsx(IconButton, {
2839
2999
  label: labels.readAloud,
2840
3000
  size: "sm",
@@ -2859,7 +3019,7 @@ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPar
2859
3019
  children: icons.retry
2860
3020
  }),
2861
3021
  actions,
2862
- /* @__PURE__ */ jsx("span", {
3022
+ time && /* @__PURE__ */ jsx("span", {
2863
3023
  className: timeClass,
2864
3024
  children: formatRelativeTime(message.createdAt, void 0, labels)
2865
3025
  })
@@ -3143,18 +3303,18 @@ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
3143
3303
  label: labels.stop,
3144
3304
  variant: "primary",
3145
3305
  onPress: onStop,
3146
- className: "rounded-chat-md [&_svg]:size-3.5 [&_svg]:fill-current",
3306
+ className: "rounded-[var(--chat-send-radius,var(--chat-radius-md))]! [&_svg]:size-3.5 [&_svg]:fill-current",
3147
3307
  children: icons.stop
3148
3308
  }) : /* @__PURE__ */ jsx(IconButton, {
3149
3309
  label: labels.send,
3150
3310
  variant: "primary",
3151
3311
  onPress: submit,
3152
3312
  isDisabled: !canSend,
3153
- className: "rounded-chat-md",
3313
+ className: "rounded-[var(--chat-send-radius,var(--chat-radius-md))]!",
3154
3314
  children: icons.send
3155
3315
  });
3156
3316
  return /* @__PURE__ */ jsxs("div", {
3157
- className: clsx("flex flex-col rounded-[1.25rem] border border-chat-border bg-chat-surface font-chat-sans shadow-chat-popover", "focus-within:border-chat-border-strong", dragging && "border-chat-accent! border-dashed", compact ? "gap-1 p-2" : "gap-2 p-3", className),
3317
+ className: clsx("flex flex-col rounded-[var(--chat-composer-radius,1.25rem)] border border-chat-border bg-chat-surface font-chat-sans shadow-chat-popover", "focus-within:border-chat-border-strong", dragging && "border-chat-accent! border-dashed", compact ? "gap-1 p-2" : "gap-2 p-3", className),
3158
3318
  ...dropProps,
3159
3319
  onPointerDown: (e) => {
3160
3320
  if (e.target === e.currentTarget) {
@@ -3198,17 +3358,128 @@ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
3198
3358
  });
3199
3359
  }
3200
3360
  /** The `+` button that opens the attach menu above the composer. */
3201
- function AttachButton({ children, label }) {
3361
+ function AttachButton({ children, label, icon, className, menuClassName }) {
3202
3362
  const icons = useIcons();
3203
3363
  const labels = useLabels();
3204
3364
  return /* @__PURE__ */ jsxs(MenuTrigger, { children: [/* @__PURE__ */ jsx(IconButton, {
3205
3365
  label: label ?? labels.addFiles,
3206
- children: icons.attach
3366
+ ...className && { className },
3367
+ children: icon ?? icons.attach
3207
3368
  }), /* @__PURE__ */ jsx(Popover, {
3208
3369
  placement: "top start",
3370
+ ...menuClassName && { className: menuClassName },
3209
3371
  children
3210
3372
  })] });
3211
3373
  }
3374
+ // src/components/effort-slider.tsx
3375
+ /**
3376
+ * Effort as a slider with one stop per level, lowest (faster) on the left: title and the current level on
3377
+ * top, the level's description under them, "Faster" and "Smarter" under the track. Arrow keys move a stop.
3378
+ */
3379
+ function EffortSlider({ efforts, value, onChange, label, className }) {
3380
+ const labels = useLabels();
3381
+ const id = useId();
3382
+ const index = Math.max(0, efforts.findIndex((e) => e.id === value));
3383
+ const current = efforts[index];
3384
+ const last = Math.max(1, efforts.length - 1);
3385
+ const at = (i) => `${i / last * 100}%`;
3386
+ if (!current) return null;
3387
+ return /* @__PURE__ */ jsxs("div", {
3388
+ className: clsx("flex w-full flex-col gap-1 font-chat-sans", className),
3389
+ children: [
3390
+ /* @__PURE__ */ jsxs("div", {
3391
+ className: "flex items-center gap-2",
3392
+ children: [
3393
+ /* @__PURE__ */ jsx("label", {
3394
+ htmlFor: id,
3395
+ className: "flex-1 text-(length:--chat-ui-font-size) font-medium text-chat-fg",
3396
+ children: label ?? labels.effort
3397
+ }),
3398
+ current.recommended && /* @__PURE__ */ jsx(Badge, { children: labels.recommended }),
3399
+ current.warning && /* @__PURE__ */ jsx(Badge, {
3400
+ tone: "warning",
3401
+ children: current.warning
3402
+ }),
3403
+ /* @__PURE__ */ jsx("output", {
3404
+ htmlFor: id,
3405
+ className: "text-(length:--chat-ui-font-size) text-chat-fg",
3406
+ children: current.label
3407
+ })
3408
+ ]
3409
+ }),
3410
+ current.description && /* @__PURE__ */ jsx("p", {
3411
+ className: "text-(length:--chat-ui-font-size-sm) leading-4 text-chat-muted",
3412
+ children: current.description
3413
+ }),
3414
+ /* @__PURE__ */ jsxs("div", {
3415
+ className: "relative mx-2 mt-2 h-6",
3416
+ children: [
3417
+ /* @__PURE__ */ jsx("div", { className: "absolute top-1/2 right-0 left-0 h-1 -translate-y-1/2 rounded-full bg-chat-border-strong" }),
3418
+ /* @__PURE__ */ jsx("div", {
3419
+ className: "absolute top-1/2 left-0 h-1 -translate-y-1/2 rounded-full bg-chat-accent",
3420
+ style: { width: at(index) }
3421
+ }),
3422
+ efforts.map((e, i) => /* @__PURE__ */ jsx("span", {
3423
+ "aria-hidden": true,
3424
+ className: clsx("absolute top-1/2 size-1.5 -translate-1/2 rounded-full", i <= index ? "bg-chat-accent-fg/70" : "bg-chat-subtle"),
3425
+ style: { left: at(i) }
3426
+ }, e.id)),
3427
+ /* @__PURE__ */ jsx("span", {
3428
+ "aria-hidden": true,
3429
+ className: "pointer-events-none absolute top-1/2 size-4 -translate-1/2 rounded-full border-2 border-chat-accent bg-chat-fg shadow-sm peer-focus-visible:ring-2",
3430
+ style: { left: at(index) }
3431
+ }),
3432
+ /* @__PURE__ */ jsx("input", {
3433
+ id,
3434
+ type: "range",
3435
+ min: 0,
3436
+ max: efforts.length - 1,
3437
+ step: 1,
3438
+ value: index,
3439
+ "aria-valuetext": current.label,
3440
+ onChange: (e) => {
3441
+ const next = efforts[Number(e.target.value)];
3442
+ if (next && next.id !== value) onChange(next.id);
3443
+ },
3444
+ onKeyDown: (e) => {
3445
+ const step = e.key === "ArrowRight" || e.key === "ArrowUp" ? 1 : e.key === "ArrowLeft" || e.key === "ArrowDown" ? -1 : 0;
3446
+ const next = step ? efforts[index + step] : void 0;
3447
+ if (next) {
3448
+ e.preventDefault();
3449
+ onChange(next.id);
3450
+ }
3451
+ },
3452
+ className: "absolute -inset-x-2 inset-y-0 w-[calc(100%+1rem)] cursor-pointer rounded-full opacity-0 focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-chat-focus [&::-webkit-slider-thumb]:opacity-0"
3453
+ })
3454
+ ]
3455
+ }),
3456
+ /* @__PURE__ */ jsxs("div", {
3457
+ className: "flex justify-between text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle",
3458
+ children: [/* @__PURE__ */ jsx("span", { children: labels.effortFaster }), /* @__PURE__ */ jsx("span", { children: labels.effortSmarter })]
3459
+ })
3460
+ ]
3461
+ });
3462
+ }
3463
+ /** A composer pill ("◎ Medium") that opens the effort slider above it, like chatgpt.com's Think button. */
3464
+ function EffortButton({ icon, children, className, ...slider }) {
3465
+ const current = slider.efforts.find((e) => e.id === slider.value) ?? slider.efforts[0];
3466
+ const labels = useLabels();
3467
+ return /* @__PURE__ */ jsxs(DialogTrigger, { children: [/* @__PURE__ */ jsxs(Button, {
3468
+ variant: "ghost",
3469
+ size: "sm",
3470
+ className: clsx("text-chat-muted data-hovered:text-chat-fg", className),
3471
+ "aria-label": `${labels.effort}: ${current?.label ?? ""}`,
3472
+ children: [icon, children ?? current?.label]
3473
+ }), /* @__PURE__ */ jsx(Popover, {
3474
+ placement: "top end",
3475
+ className: "w-72 p-3",
3476
+ children: /* @__PURE__ */ jsx(Dialog, {
3477
+ "aria-label": labels.effort,
3478
+ className: "outline-none",
3479
+ children: /* @__PURE__ */ jsx(EffortSlider, { ...slider })
3480
+ })
3481
+ })] });
3482
+ }
3212
3483
  // src/components/model-picker.tsx
3213
3484
  /** "Opus 5.5 High ⌄": the models, an Effort submenu for models that have levels, and More models. */
3214
3485
  function ModelPicker({ models, model, onModelChange, effort, onEffortChange, effortNote, footnote, className }) {
@@ -3323,12 +3594,13 @@ const STICK_SLOP = 48;
3323
3594
  * The scrolling conversation. It follows a streaming reply while the reader is at the bottom and leaves
3324
3595
  * them alone once they scroll up, with a button to jump back. Sending a message always jumps down.
3325
3596
  */
3326
- function MessageList({ messages, onRetry, onEdit, onFeedback, renderPart, header, footer, start = "bottom", className }) {
3597
+ function MessageList({ messages, onRetry, onEdit, onFeedback, onReadAloud, actions, renderPart, header, footer, start = "bottom", className }) {
3327
3598
  const icons = useIcons();
3328
3599
  const scroller = useRef(null);
3329
3600
  const content = useRef(null);
3330
3601
  const stuck = useRef(start === "bottom");
3331
3602
  const labels = useLabels();
3603
+ const later = useMemo(() => laterReplacements(messages), [messages]);
3332
3604
  const [atBottom, setAtBottom] = useState(true);
3333
3605
  const toBottom = useCallback((behavior = "auto") => {
3334
3606
  const el = scroller.current;
@@ -3378,15 +3650,25 @@ function MessageList({ messages, onRetry, onEdit, onFeedback, renderPart, header
3378
3650
  role: "log",
3379
3651
  "aria-label": labels.conversation,
3380
3652
  className: "mx-auto flex w-full max-w-[calc(var(--chat-content-width)+2*var(--chat-gutter,var(--chat-content-padding)))] flex-1 flex-col gap-6 px-[var(--chat-gutter,var(--chat-content-padding))] pt-6 pb-4",
3381
- children: [header, messages.map((m) => m.role === "user" ? /* @__PURE__ */ jsx(UserMessage, {
3382
- message: m,
3383
- ...onEdit && { onEdit: () => onEdit(m.id) }
3384
- }, m.id) : /* @__PURE__ */ jsx(AssistantMessage, {
3385
- message: m,
3386
- ...onRetry && { onRetry: () => onRetry(m.id) },
3387
- ...onFeedback && { onFeedback: (f) => onFeedback(m.id, f) },
3388
- ...renderPart && { renderPart }
3389
- }, m.id))]
3653
+ children: [header, /* @__PURE__ */ jsx(LaterReplacements, {
3654
+ value: later,
3655
+ children: messages.map((m) => m.role === "user" ? /* @__PURE__ */ jsx(UserMessage, {
3656
+ message: m,
3657
+ ...onEdit && { onEdit: () => onEdit(m.id) },
3658
+ ...actions?.extra && { actions: actions.extra(m) },
3659
+ ...actions?.copy === false && { copy: false },
3660
+ ...actions?.time === false && { time: false }
3661
+ }, m.id) : /* @__PURE__ */ jsx(AssistantMessage, {
3662
+ message: m,
3663
+ ...onRetry && { onRetry: () => onRetry(m.id) },
3664
+ ...onFeedback && { onFeedback: (f) => onFeedback(m.id, f) },
3665
+ ...onReadAloud && { onReadAloud: () => onReadAloud(m.id) },
3666
+ ...actions?.extra && { actions: actions.extra(m) },
3667
+ ...actions?.copy === false && { copy: false },
3668
+ ...actions?.time === false && { time: false },
3669
+ ...renderPart && { renderPart }
3670
+ }, m.id))
3671
+ })]
3390
3672
  }), footer && /* @__PURE__ */ jsx("div", {
3391
3673
  className: "sticky bottom-0",
3392
3674
  children: footer
@@ -3683,7 +3965,7 @@ const densityVars = {
3683
3965
  }
3684
3966
  };
3685
3967
  /** A whole chat: header, the conversation, the docked composer. An empty chat shows `emptyState`. */
3686
- function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove, onRetry, onEdit, onFeedback, renderPart, widgets, header, composer, emptyState, disclaimer, density = "full", contentWidth, contentPadding, className }) {
3968
+ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove, onRetry, onEdit, onFeedback, onReadAloud, messageActions, renderPart, widgets, header, composer, emptyState, disclaimer, renderComposer, density = "full", contentWidth, contentPadding, className }) {
3687
3969
  const style = {
3688
3970
  ...densityVars[density],
3689
3971
  ...contentWidth && { "--chat-content-width": contentWidth },
@@ -3698,14 +3980,17 @@ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove
3698
3980
  const [body, short] = useShortPane();
3699
3981
  const labels = useLabels();
3700
3982
  const pending = onApprove && approvals?.[0];
3701
- const box = (variant) => /* @__PURE__ */ jsx(Composer, {
3702
- variant: density === "inline" || short && !empty ? "compact" : variant,
3703
- busy,
3704
- onSubmit: onSend,
3705
- ...onStop && { onStop },
3706
- ...composer,
3707
- ...pending && { placeholder: labels.replyDirectly }
3708
- });
3983
+ const box = (variant) => {
3984
+ const element = /* @__PURE__ */ jsx(Composer, {
3985
+ variant: density === "inline" || short && !empty ? "compact" : variant,
3986
+ busy,
3987
+ onSubmit: onSend,
3988
+ ...onStop && { onStop },
3989
+ ...composer,
3990
+ ...pending && { placeholder: labels.replyDirectly }
3991
+ });
3992
+ return renderComposer ? renderComposer(element, variant === "centered" ? "empty" : "docked") : element;
3993
+ };
3709
3994
  return /* @__PURE__ */ jsxs("div", {
3710
3995
  "data-chat-density": density,
3711
3996
  style,
@@ -3726,6 +4011,8 @@ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove
3726
4011
  ...onRetry && { onRetry },
3727
4012
  ...onEdit && { onEdit },
3728
4013
  ...onFeedback && { onFeedback },
4014
+ ...onReadAloud && { onReadAloud },
4015
+ ...messageActions && { actions: messageActions },
3729
4016
  ...renderPart && { renderPart },
3730
4017
  footer: /* @__PURE__ */ jsx("div", {
3731
4018
  className: clsx("bg-linear-to-t from-chat-bg from-70% to-transparent px-(--chat-gutter)", short ? "pt-2" : "pt-4"),
@@ -3771,7 +4058,7 @@ function EmptyState({ greeting, icon, children, footer }) {
3771
4058
  /* @__PURE__ */ jsxs("h1", {
3772
4059
  className: "chat-empty-greeting flex items-center justify-center gap-3 text-center font-chat-serif text-[clamp(1.5rem,6cqw,2.5rem)] font-normal text-chat-fg",
3773
4060
  children: [icon && /* @__PURE__ */ jsx("span", {
3774
- className: "flex shrink-0 text-chat-accent [&_svg]:size-[1em]",
4061
+ className: "flex shrink-0 text-[var(--chat-empty-icon,var(--chat-accent))] [&_svg]:size-[1em]",
3775
4062
  children: icon
3776
4063
  }), greeting]
3777
4064
  }),
@@ -3892,17 +4179,27 @@ function SidebarToggle({ drawerOnly = false, className }) {
3892
4179
  }
3893
4180
  // src/views/sidebar.tsx
3894
4181
  /** The claude.ai sidebar frame. Collapses to an icon rail inside `AppShell`; a drawer on phones. */
3895
- function Sidebar({ brand, headerActions, nav, children, footer, className }) {
4182
+ function Sidebar({ brand, brandIcon, headerActions, nav, children, footer, className }) {
3896
4183
  const { collapsed } = useSidebar();
3897
4184
  return /* @__PURE__ */ jsxs("div", {
3898
4185
  className: clsx("flex h-full flex-col font-chat-sans", collapsed ? "w-13" : "w-full", className),
3899
4186
  children: [
4187
+ collapsed && brandIcon && /* @__PURE__ */ jsx("div", {
4188
+ className: "flex h-12 shrink-0 items-center justify-center [&_svg]:size-5",
4189
+ children: brandIcon
4190
+ }),
3900
4191
  /* @__PURE__ */ jsxs("div", {
3901
4192
  className: clsx("flex h-12 shrink-0 items-center gap-1 px-2", collapsed && "justify-center"),
3902
4193
  children: [
3903
- !collapsed && /* @__PURE__ */ jsx("div", {
3904
- className: "min-w-0 flex-1 truncate px-1.5 font-chat-serif text-lg text-chat-fg",
3905
- children: brand
4194
+ !collapsed && /* @__PURE__ */ jsxs("div", {
4195
+ className: "flex min-w-0 flex-1 items-center gap-2 px-1.5 font-chat-serif text-lg text-chat-fg",
4196
+ children: [brandIcon && /* @__PURE__ */ jsx("span", {
4197
+ className: "flex shrink-0 [&_svg]:size-5",
4198
+ children: brandIcon
4199
+ }), /* @__PURE__ */ jsx("span", {
4200
+ className: "min-w-0 truncate",
4201
+ children: brand
4202
+ })]
3906
4203
  }),
3907
4204
  !collapsed && headerActions,
3908
4205
  /* @__PURE__ */ jsx(SidebarToggle, {})
@@ -4002,4 +4299,4 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
4002
4299
  children: menu
4003
4300
  })] });
4004
4301
  }
4005
- export { ActivityGroup, AgentProvider, AgentRegistry, AppShell, ApprovalCard, AssistantMessage, AttachButton, AttachmentCard, AttachmentRow, Badge, Button, Caret, ChatHeader, ChatList, ChatNotice, ChatTheme, ChatTitleMenu, ChatTranscript, ChatView, CodeBlock, Composer, ContextMenu, ContextPanel, DRAWER_QUERY, Disclosure, Drawer, EmptyState, IconButton, Markdown, MarkdownLinksProvider, Menu, MenuItem, MenuNote, MenuSearch, MenuSection, MenuSeparator, MenuTrigger, MessageList, Modal, ModelPicker, Panel, PastedTextDialog, Popover, Sidebar, SidebarItem, SidebarToggle, SidebarUser, SubmenuTrigger, TokenCounter, ToolViewsProvider, Tooltip, UiComponentsProvider, UiMarkupBlock, UiTree, UserMessage, WIDGET_MAX_HEIGHT, WidgetFrame, WidgetHostProvider, WidgetPartView, activityHeading, activityLabel, commandTool, defaultIcons, defaultLabels, defaultToolViews, formatDuration, formatRelativeTime, formatTokens, htmlComponent, intersectAllow, memoryTool, messageText, openInNewTab, readTreeInput, readVisualizeInput, sseTransport, uiMarkupToolView, uiTreeToolView, useAgentContext, useAgentRegistry, useAgentSnapshot, useAgentTool, useAttachments, useChat, useChatTool, useCountUp, useIcons, useLabels, useMediaQuery, useSidebar, useSmoothText, useToolView, useToolViews, useUiAction, useUiMarkupLanguage, useUiState, useWidgetHost, userMessage, webFetchTool, webSearchTool, widgetCsp, widgetToolView };
4302
+ export { ActivityGroup, AgentProvider, AgentRegistry, AppShell, ApprovalCard, AssistantMessage, AttachButton, AttachmentCard, AttachmentRow, Badge, Button, Caret, ChatHeader, ChatList, ChatNotice, ChatTheme, ChatTitleMenu, ChatTranscript, ChatView, CodeBlock, Composer, ContextMenu, ContextPanel, CopyButton, DRAWER_QUERY, Disclosure, Drawer, EffortButton, EffortSlider, EmptyState, IconButton, Markdown, MarkdownLinksProvider, Menu, MenuItem, MenuNote, MenuSearch, MenuSection, MenuSeparator, MenuTrigger, MessageList, Modal, ModelPicker, Panel, PastedTextDialog, Popover, SegmentedControl, Sidebar, SidebarItem, SidebarToggle, SidebarUser, SubmenuTrigger, TokenCounter, ToolViewsProvider, Tooltip, UiComponentsProvider, UiMarkupBlock, UiTree, UserMessage, WIDGET_MAX_HEIGHT, WidgetFrame, WidgetHostProvider, WidgetPartView, activityHeading, activityLabel, commandTool, defaultIcons, defaultLabels, defaultToolViews, formatDuration, formatRelativeTime, formatTokens, htmlComponent, intersectAllow, memoryTool, messageText, openInNewTab, readTreeInput, readVisualizeInput, sseTransport, uiMarkupToolView, uiTreeToolView, useAgentContext, useAgentRegistry, useAgentSnapshot, useAgentTool, useAttachments, useChat, useChatTool, useCountUp, useIcons, useLabels, useMediaQuery, useSidebar, useSmoothText, useToolView, useToolViews, useUiAction, useUiMarkupLanguage, useUiState, useWidgetHost, userMessage, webFetchTool, webSearchTool, widgetCsp, widgetToolView };