agentic-ui-kit 0.6.0 → 0.7.1

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 (28) hide show
  1. package/dist/index.js +719 -123
  2. package/dist/styles.css +1 -1
  3. package/dist/tailwind.css +49 -0
  4. package/dist/types/react/src/components/attachments.d.ts +4 -1
  5. package/dist/types/react/src/components/chat-list.d.ts +16 -0
  6. package/dist/types/react/src/components/context-meter.d.ts +24 -0
  7. package/dist/types/react/src/components/context-panel.d.ts +5 -0
  8. package/dist/types/react/src/components/copy-field.d.ts +15 -0
  9. package/dist/types/react/src/components/markdown.d.ts +19 -2
  10. package/dist/types/react/src/components/model-picker.d.ts +6 -1
  11. package/dist/types/react/src/components/tools.d.ts +7 -2
  12. package/dist/types/react/src/hooks/sidebar-state.d.ts +6 -0
  13. package/dist/types/react/src/hooks/sse-transport.d.ts +7 -1
  14. package/dist/types/react/src/hooks/use-chat.d.ts +14 -1
  15. package/dist/types/react/src/index.d.ts +11 -7
  16. package/dist/types/react/src/primitives/button.d.ts +11 -3
  17. package/dist/types/react/src/primitives/chat-theme.d.ts +3 -1
  18. package/dist/types/react/src/primitives/cx.d.ts +3 -0
  19. package/dist/types/react/src/primitives/icons.d.ts +2 -0
  20. package/dist/types/react/src/primitives/labels.d.ts +18 -0
  21. package/dist/types/react/src/primitives/menu.d.ts +23 -5
  22. package/dist/types/react/src/primitives/overlay.d.ts +13 -1
  23. package/dist/types/react/src/primitives/panel.d.ts +3 -1
  24. package/dist/types/react/src/primitives/segmented-control.d.ts +29 -4
  25. package/dist/types/react/src/views/app-shell.d.ts +31 -3
  26. package/dist/types/react/src/views/chat-view.d.ts +6 -2
  27. package/dist/types/react/src/views/sidebar.d.ts +35 -6
  28. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -1,13 +1,40 @@
1
1
  "use client";
2
2
  import { A as awaitingToolCalls, C as widgetLoaderMessages, D as toBlocks, F as settleToolCall, L as salvageJson, O as activityTokens, T as readEventStream, _ as replacedId, b as resolveReplaces, d as isStateRef, f as matchesSchema, g as uiActionSchema, i as parseUiMarkup, k as applyEvent } from "./src-D01l3nBc.js";
3
3
  import { clsx } from "clsx";
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
- 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";
7
- import { AlertTriangle, ArrowDown, ArrowUp, Check, ChevronDown, ChevronRight, ChevronsUpDown, Copy, FileText, Folder, FolderOpen, FolderPlus, Globe, Info, Mic, MoreHorizontal, PanelLeft, Pencil, Pin, PinOff, Plus, RefreshCw, Share, ShieldQuestion, Square, ThumbsDown, ThumbsUp, Trash2, Volume2, X } from "lucide-react";
4
+ import { extendTailwindMerge } from "tailwind-merge";
5
+ import { Children, Component, Fragment, createContext, createElement, isValidElement, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
6
+ import { Autocomplete, Button as Button$1, Dialog, DialogTrigger, DialogTrigger as DialogTrigger$1, 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";
7
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
+ import { AlertTriangle, ArrowDown, ArrowUp, Check, ChevronDown, ChevronRight, ChevronsUpDown, Copy, FileText, Folder, FolderOpen, FolderPlus, Globe, Info, MessageSquare, Mic, MoreHorizontal, PanelLeft, Pencil, Pin, PinOff, Plus, RefreshCw, Share, ShieldQuestion, Square, ThumbsDown, ThumbsUp, Trash2, Volume2, X } from "lucide-react";
8
9
  import { UNSAFE_PortalProvider } from "react-aria";
9
10
  import { createMathPlugin } from "@streamdown/math";
10
- import { Streamdown, defaultRehypePlugins } from "streamdown";
11
+ import { Streamdown, defaultComponents, defaultRehypePlugins } from "streamdown";
12
+ // src/primitives/cx.ts
13
+ const twMerge = extendTailwindMerge({ extend: { theme: {
14
+ radius: [
15
+ "chat-sm",
16
+ "chat-md",
17
+ "chat-lg",
18
+ "chat-popover"
19
+ ],
20
+ shadow: ["chat-popover"],
21
+ animate: [
22
+ "chat-in",
23
+ "chat-out",
24
+ "chat-fade-in",
25
+ "chat-fade-out",
26
+ "chat-drawer-in",
27
+ "chat-drawer-out",
28
+ "chat-drawer-in-right",
29
+ "chat-drawer-out-right",
30
+ "chat-sheet-in",
31
+ "chat-sheet-out"
32
+ ]
33
+ } } });
34
+ /** `clsx` plus tailwind-merge: later classes win over earlier ones that set the same property, so a caller's `h-7` or `rounded-none` beats the kit's. */
35
+ function cx(...inputs) {
36
+ return twMerge(clsx(inputs));
37
+ }
11
38
  // src/primitives/tooltip.tsx
12
39
  function Tooltip({ content, children, placement = "top", delay = 600 }) {
13
40
  return /* @__PURE__ */ jsxs(TooltipTrigger, {
@@ -38,23 +65,88 @@ const variants = {
38
65
  */
39
66
  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"
40
67
  };
41
- const sizes = {
68
+ const sizes$1 = {
42
69
  sm: "h-7 gap-1.5 px-2 text-(length:--chat-ui-font-size) leading-5 [@media(pointer:coarse)]:h-9",
43
70
  md: "h-8 gap-2 px-2.5 text-(length:--chat-ui-font-size) leading-5 [@media(pointer:coarse)]:h-10",
44
71
  /** Dialog and empty-state actions. */
45
72
  lg: "h-9 gap-2 px-3.5 text-(length:--chat-ui-font-size) leading-5 [@media(pointer:coarse)]:h-11"
46
73
  };
47
- function Button({ variant = "ghost", size = "md", className, ...props }) {
48
- return /* @__PURE__ */ jsx(Button$1, {
74
+ /** Press handlers dropped from a button that is disabled with a reason; it only looks and announces as disabled. */
75
+ const pressHandlers = [
76
+ "onPress",
77
+ "onPressStart",
78
+ "onPressEnd",
79
+ "onPressChange",
80
+ "onPressUp",
81
+ "onClick"
82
+ ];
83
+ /**
84
+ * A disabled button that still explains itself. React Aria's `isDisabled` makes the button unfocusable and
85
+ * hoverable-less, so there'd be nothing to hang a tooltip on. With a reason the button instead stays focusable
86
+ * with `aria-disabled`, loses its press handlers and never submits a form, and the reason is shown as a tooltip
87
+ * and read as its description.
88
+ */
89
+ function useDisabledReason(props, disabledReason) {
90
+ const id = useId();
91
+ const active = Boolean(props.isDisabled && disabledReason);
92
+ if (!active) return {
93
+ active,
94
+ props,
95
+ reasonId: void 0
96
+ };
97
+ const rest = {
49
98
  ...props,
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)
99
+ isDisabled: false,
100
+ type: "button"
101
+ };
102
+ for (const h of pressHandlers) delete rest[h];
103
+ rest["aria-disabled"] = true;
104
+ rest["data-disabled"] = true;
105
+ rest["aria-describedby"] = [props["aria-describedby"], id].filter(Boolean).join(" ");
106
+ return {
107
+ active,
108
+ props: rest,
109
+ reasonId: id
110
+ };
111
+ }
112
+ /** Hover and press styles are dropped while a button is disabled with a reason, to look as disabled as one that is. */
113
+ const noInteraction = (classes) => classes.replace(/data-(hovered|pressed):\S+\s*/g, "");
114
+ function WithReason({ active, reasonId, reason, children }) {
115
+ if (!active) return children;
116
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Tooltip, {
117
+ content: reason,
118
+ children
119
+ }), /* @__PURE__ */ jsx("span", {
120
+ id: reasonId,
121
+ hidden: true,
122
+ children: reason
123
+ })] });
124
+ }
125
+ function Button({ variant = "ghost", size = "md", className, disabledReason, ...rest }) {
126
+ const { active, props, reasonId } = useDisabledReason(rest, disabledReason);
127
+ const button = /* @__PURE__ */ jsx(Button$1, {
128
+ ...props,
129
+ className: cx("inline-flex cursor-pointer items-center justify-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, active ? noInteraction(variants[variant]) : variants[variant], sizes$1[size], className)
130
+ });
131
+ return /* @__PURE__ */ jsx(WithReason, {
132
+ active,
133
+ reasonId,
134
+ reason: disabledReason,
135
+ children: button
51
136
  });
52
137
  }
53
- function IconButton({ label, variant = "ghost", size = "md", tooltip = true, className, ...props }) {
138
+ function IconButton({ label, variant = "ghost", size = "md", tooltip = true, className, disabledReason, ...rest }) {
139
+ const { active, props, reasonId } = useDisabledReason(rest, disabledReason);
54
140
  const button = /* @__PURE__ */ jsx(Button$1, {
55
141
  ...props,
56
142
  "aria-label": label,
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)
143
+ className: cx("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, active ? noInteraction(variants[variant]) : variants[variant], variant === "ghost" && (active ? "text-chat-muted" : "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)
144
+ });
145
+ if (active) return /* @__PURE__ */ jsx(WithReason, {
146
+ active: true,
147
+ reasonId,
148
+ reason: disabledReason,
149
+ children: button
58
150
  });
59
151
  return tooltip ? /* @__PURE__ */ jsx(Tooltip, {
60
152
  content: label,
@@ -70,7 +162,7 @@ const tones = {
70
162
  };
71
163
  function Badge({ children, tone = "outline", icon, className }) {
72
164
  return /* @__PURE__ */ jsxs("span", {
73
- className: clsx("inline-flex items-center gap-1 rounded-full px-2 py-px font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 whitespace-nowrap [&_svg]:size-3", tones[tone], className),
165
+ className: cx("inline-flex items-center gap-1 rounded-full px-2 py-px font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 whitespace-nowrap [&_svg]:size-3", tones[tone], className),
74
166
  children: [icon, children]
75
167
  });
76
168
  }
@@ -124,7 +216,8 @@ const defaultIcons = {
124
216
  folder: /* @__PURE__ */ jsx(Folder, {}),
125
217
  folderOpen: /* @__PURE__ */ jsx(FolderOpen, {}),
126
218
  file: /* @__PURE__ */ jsx(FileText, {}),
127
- remove: /* @__PURE__ */ jsx(X, {})
219
+ remove: /* @__PURE__ */ jsx(X, {}),
220
+ chatList: /* @__PURE__ */ jsx(MessageSquare, {})
128
221
  };
129
222
  const IconsContext = createContext(defaultIcons);
130
223
  /** Merges `icons` over the icons of the enclosing provider (or the defaults). */
@@ -170,6 +263,9 @@ const defaultLabels = {
170
263
  duration: (seconds) => seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`,
171
264
  ranCommandLabel: "Ran a command",
172
265
  ranCommands: (count) => plural(count, "ran a command", (n) => `ran ${n} commands`),
266
+ runCommandAsk: "Run this command?",
267
+ searchWebAsk: "Search the web?",
268
+ readPageAsk: "Read this page?",
173
269
  searchedWebLabel: "Searched the web",
174
270
  readPageLabel: "Read page",
175
271
  readPages: (count) => plural(count, "read a page", (n) => `read ${n} pages`),
@@ -181,7 +277,7 @@ const defaultLabels = {
181
277
  noResults: "No results",
182
278
  reasoningHidden: "The model's reasoning is not shown.",
183
279
  allowTool: (name) => `Allow ${name}?`,
184
- allowAction: (label) => /* @__PURE__ */ jsxs(Fragment, { children: [
280
+ allowAction: (label) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
185
281
  "Allow ",
186
282
  label,
187
283
  "?"
@@ -193,9 +289,14 @@ const defaultLabels = {
193
289
  openSidebar: "Open sidebar",
194
290
  expandSidebar: "Expand sidebar",
195
291
  collapseSidebar: "Collapse sidebar",
292
+ resizeSidebar: "Resize sidebar",
293
+ withShortcut: (label, keys) => `${label} (${keys})`,
294
+ ctrlKey: "Ctrl",
196
295
  context: "Context",
197
296
  contextUsed: "Context used",
198
297
  percentUsed: (percent) => `${percent}% used`,
298
+ contextSummary: (used, limit, percent) => `Context: ${used} / ${limit} (${percent}%)`,
299
+ contextLoading: "Context: loading",
199
300
  freeSpace: "Free space",
200
301
  tokenCount: (count) => `${count} tokens`,
201
302
  replyPlaceholder: "Reply…",
@@ -240,6 +341,7 @@ const defaultLabels = {
240
341
  justNow: "just now",
241
342
  copy: "Copy",
242
343
  copied: "Copied",
344
+ copyFieldLabel: "Link",
243
345
  retry: "Retry",
244
346
  edit: "Edit",
245
347
  readAloud: "Read aloud",
@@ -299,27 +401,40 @@ function CopyButton({ text, className }) {
299
401
  });
300
402
  }
301
403
  // src/primitives/segmented-control.tsx
404
+ const sizes = {
405
+ sm: {
406
+ button: "h-6",
407
+ iconOnly: "w-7"
408
+ },
409
+ md: {
410
+ button: "h-8",
411
+ iconOnly: "w-9"
412
+ }
413
+ };
302
414
  /**
303
415
  * One of a few options, side by side on a track, similar to claude.ai's Chat / Cowork switch. Themed with
304
416
  * `--chat-segment-track` (default: a shade darker than the surface it sits on) and `--chat-segment-selected`
305
417
  * (default `--chat-raised`).
306
418
  */
307
- function SegmentedControl({ label, options, value, onChange, className }) {
419
+ function SegmentedControl({ label, options, value, onChange, clearable, isDisabled, size = "sm", className }) {
308
420
  return /* @__PURE__ */ jsx(ToggleButtonGroup, {
309
421
  "aria-label": label,
310
422
  selectionMode: "single",
311
- disallowEmptySelection: true,
312
- selectedKeys: [value],
423
+ disallowEmptySelection: !clearable,
424
+ ...isDisabled && { isDisabled },
425
+ selectedKeys: value === null ? [] : [value],
313
426
  onSelectionChange: (keys) => {
314
427
  const next = [...keys][0];
315
428
  if (next !== void 0) onChange(next);
429
+ else if (clearable) onChange(null);
316
430
  },
317
- 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),
431
+ className: cx("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),
318
432
  children: options.map((o) => {
319
433
  const button = /* @__PURE__ */ jsxs(ToggleButton, {
320
434
  id: o.id,
435
+ ...o.isDisabled && { isDisabled: true },
321
436
  ...o.iconOnly && { "aria-label": o.label },
322
- 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)]"),
437
+ className: cx("inline-flex 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", sizes[size].button, o.iconOnly ? sizes[size].iconOnly : "px-2", "data-disabled:cursor-default data-disabled:opacity-40", "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)]"),
323
438
  children: [o.icon, !o.iconOnly && o.label]
324
439
  }, o.id);
325
440
  return o.iconOnly ? /* @__PURE__ */ jsx(Tooltip, {
@@ -340,7 +455,7 @@ function Disclosure({ summary, children, className, summaryClassName, panelClass
340
455
  className: "m-0 font-normal",
341
456
  children: /* @__PURE__ */ jsxs(Button$1, {
342
457
  slot: "trigger",
343
- className: clsx("group/trigger flex max-w-full cursor-pointer items-center gap-1.5 rounded-chat-sm text-left text-chat-muted outline-none", "data-hovered:text-chat-fg data-focus-visible:ring-2 data-focus-visible:ring-chat-focus", summaryClassName),
458
+ className: cx("group/trigger flex max-w-full cursor-pointer items-center gap-1.5 rounded-chat-sm text-left text-chat-muted outline-none", "data-hovered:text-chat-fg data-focus-visible:ring-2 data-focus-visible:ring-chat-focus", summaryClassName),
344
459
  children: [
345
460
  indicator === "chevron" && /* @__PURE__ */ jsx(IconSlot, {
346
461
  className: "transition-transform group-aria-expanded/trigger:rotate-90 [&_svg]:size-3.5",
@@ -367,6 +482,11 @@ function Disclosure({ summary, children, className, summaryClassName, panelClass
367
482
  });
368
483
  }
369
484
  // src/primitives/menu.tsx
485
+ /**
486
+ * Wraps a trigger button and a kit `Popover` (or `Modal`) so that pressing the button toggles it, without
487
+ * importing react-aria. The same component as `DialogTrigger`; use whichever name reads better.
488
+ */
489
+ const PopoverTrigger = DialogTrigger$1;
370
490
  /** A search box above a menu that filters its items, like "Search or create a project". */
371
491
  function MenuSearch({ placeholder, children, value, onChange }) {
372
492
  const { contains } = useFilter({ sensitivity: "base" });
@@ -385,41 +505,71 @@ function MenuSearch({ placeholder, children, value, onChange }) {
385
505
  }), children]
386
506
  });
387
507
  }
388
- /** Floating panel for menus and pickers: surface color, rounded, soft shadow, fades in. */
389
- function Popover({ className, offset = 6, ...props }) {
508
+ /** Viewport narrower than `max` px. False on the server and where `matchMedia` is missing. */
509
+ function useNarrower(max) {
510
+ const query = max === false ? null : `(max-width: ${max - .02}px)`;
511
+ return useSyncExternalStore((notify) => {
512
+ if (!query || typeof window === "undefined" || !window.matchMedia) return () => {};
513
+ const list = window.matchMedia(query);
514
+ list.addEventListener("change", notify);
515
+ return () => list.removeEventListener("change", notify);
516
+ }, () => query && typeof window !== "undefined" && window.matchMedia ? window.matchMedia(query).matches : false, () => false);
517
+ }
518
+ /** Floating panel for menus and pickers: surface color, rounded, soft shadow, fades in. On phones it's a bottom sheet. */
519
+ function Popover({ className, offset = 6, sheet = 768, width = "auto", fullWidthOnPhones, children, ...props }) {
520
+ if (useNarrower(sheet)) {
521
+ const { isOpen, onOpenChange } = props;
522
+ return /* @__PURE__ */ jsx(ModalOverlay, {
523
+ ...isOpen !== void 0 && { isOpen },
524
+ ...onOpenChange && { onOpenChange },
525
+ isDismissable: true,
526
+ className: "fixed inset-0 z-50 flex items-end bg-(--chat-backdrop) data-entering:animate-chat-fade-in data-exiting:animate-chat-fade-out",
527
+ children: /* @__PURE__ */ jsx(Modal$1, {
528
+ className: cx("flex max-h-[85dvh] w-full flex-col overflow-auto rounded-t-chat-lg border-t border-chat-popover-border bg-chat-popover pb-[env(safe-area-inset-bottom)] font-chat-sans text-chat-fg shadow-chat-popover outline-none", "data-entering:animate-chat-sheet-in data-exiting:animate-chat-sheet-out", className),
529
+ children: typeof children === "function" ? children({
530
+ placement: "bottom",
531
+ trigger: null,
532
+ isEntering: false,
533
+ isExiting: false,
534
+ defaultChildren: void 0
535
+ }) : children
536
+ })
537
+ });
538
+ }
390
539
  return /* @__PURE__ */ jsx(Popover$1, {
391
540
  offset,
392
541
  ...props,
393
- className: clsx("rounded-chat-popover border border-chat-popover-border bg-chat-popover font-chat-sans text-chat-fg shadow-chat-popover outline-none", "motion-safe:data-entering:animate-chat-in motion-safe:data-exiting:animate-chat-out", className)
542
+ className: cx("rounded-chat-popover border border-chat-popover-border bg-chat-popover font-chat-sans text-chat-fg shadow-chat-popover outline-none", "motion-safe:data-entering:animate-chat-in motion-safe:data-exiting:animate-chat-out", width === "trigger" && "w-(--trigger-width) [&_[role=menu]]:min-w-0", fullWidthOnPhones && "max-sm:w-[calc(100vw-1.5rem)]", className),
543
+ children
394
544
  });
395
545
  }
396
546
  function Menu({ className, ...props }) {
397
547
  return /* @__PURE__ */ jsx(Menu$1, {
398
548
  ...props,
399
- className: clsx("max-h-[inherit] min-w-52 overflow-auto p-1.5 outline-none", className)
549
+ className: cx("max-h-[inherit] min-w-52 overflow-auto p-1.5 outline-none", className)
400
550
  });
401
551
  }
402
- function MenuItem({ children, icon, description, descriptionPlacement = "below", detail, shortcut, tone = "default", className, ...props }) {
552
+ function MenuItem({ children, icon, description, descriptionPlacement = "below", detail, shortcut, tone = "default", indicator = "check", className, ...props }) {
403
553
  const icons = useIcons();
404
554
  const textValue = props.textValue ?? (typeof children === "string" ? children : void 0);
405
555
  return /* @__PURE__ */ jsx(MenuItem$1, {
406
556
  ...props,
407
557
  ...textValue !== void 0 && { textValue },
408
- className: clsx("group flex min-h-7 cursor-pointer items-center gap-2.5 rounded-chat-sm px-2 py-1 text-(length:--chat-ui-font-size) leading-5 text-chat-fg outline-none select-none", "data-focused:bg-chat-popover-hover data-open:bg-chat-popover-hover data-disabled:opacity-40", tone === "danger" && "text-chat-danger", className),
409
- children: ({ isSelected, hasSubmenu, selectionMode }) => /* @__PURE__ */ jsxs(Fragment, { children: [
558
+ className: cx("group flex min-h-7 cursor-pointer items-center gap-2.5 rounded-chat-sm px-2 py-1 text-(length:--chat-ui-font-size) leading-5 text-chat-fg outline-none select-none", "data-focused:bg-chat-popover-hover data-open:bg-chat-popover-hover data-disabled:opacity-40", tone === "danger" && "text-chat-danger", className),
559
+ children: ({ isSelected, hasSubmenu, selectionMode }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
410
560
  icon && /* @__PURE__ */ jsx("span", {
411
- className: clsx("flex size-4 shrink-0 items-center [&_svg]:size-4", tone === "danger" ? "text-chat-danger" : "text-chat-fg"),
561
+ className: cx("flex size-4 shrink-0 items-center [&_svg]:size-4", tone === "danger" ? "text-chat-danger" : "text-chat-fg"),
412
562
  children: icon
413
563
  }),
414
564
  /* @__PURE__ */ jsxs("span", {
415
- className: clsx("flex min-w-0 flex-1", descriptionPlacement === "beside" ? "items-baseline gap-2" : "flex-col"),
565
+ className: cx("flex min-w-0 flex-1", descriptionPlacement === "beside" ? "items-baseline gap-2" : "flex-col"),
416
566
  children: [/* @__PURE__ */ jsx(Text, {
417
567
  slot: "label",
418
- className: clsx("truncate", descriptionPlacement === "beside" && "shrink-0"),
568
+ className: cx("truncate", descriptionPlacement === "beside" && "shrink-0"),
419
569
  children
420
570
  }), description && /* @__PURE__ */ jsx(Text, {
421
571
  slot: "description",
422
- 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"),
572
+ className: cx("text-chat-muted", descriptionPlacement === "beside" ? "truncate text-(length:--chat-ui-font-size) leading-5" : "text-(length:--chat-ui-font-size-sm) leading-4"),
423
573
  children: description
424
574
  })]
425
575
  }),
@@ -431,7 +581,12 @@ function MenuItem({ children, icon, description, descriptionPlacement = "below",
431
581
  className: "shrink-0 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle",
432
582
  children: shortcut
433
583
  }),
434
- selectionMode !== "none" && isSelected && /* @__PURE__ */ jsx(IconSlot, {
584
+ selectionMode !== "none" && indicator === "switch" && /* @__PURE__ */ jsx("span", {
585
+ "aria-hidden": true,
586
+ "data-selected": isSelected || void 0,
587
+ className: "relative h-4 w-7 shrink-0 rounded-full bg-chat-border-strong transition-colors after:absolute after:top-0.5 after:left-0.5 after:size-3 after:rounded-full after:bg-chat-bg after:transition-transform data-selected:bg-chat-accent data-selected:after:translate-x-3 data-selected:after:bg-chat-accent-fg"
588
+ }),
589
+ selectionMode !== "none" && indicator === "check" && isSelected && /* @__PURE__ */ jsx(IconSlot, {
435
590
  className: "text-chat-check [&_svg]:size-4",
436
591
  children: icons.check
437
592
  }),
@@ -458,7 +613,7 @@ function MenuSeparator() {
458
613
  /** Explanatory text inside a menu, above or below the items. Not focusable. */
459
614
  function MenuNote({ children, className }) {
460
615
  return /* @__PURE__ */ jsx("div", {
461
- className: clsx("px-2 py-1.5 text-(length:--chat-ui-font-size) leading-snug text-chat-muted", className),
616
+ className: cx("px-2 py-1.5 text-(length:--chat-ui-font-size) leading-snug text-chat-muted", className),
462
617
  children
463
618
  });
464
619
  }
@@ -492,7 +647,7 @@ function ContextMenu({ menu, children, className }) {
492
647
  ref: root,
493
648
  onContextMenu,
494
649
  onKeyDown,
495
- className: clsx("relative", className),
650
+ className: cx("relative", className),
496
651
  children: [children, /* @__PURE__ */ jsxs(MenuTrigger$1, {
497
652
  isOpen: at !== void 0,
498
653
  onOpenChange: (open) => !open && setAt(void 0),
@@ -513,15 +668,23 @@ function ContextMenu({ menu, children, className }) {
513
668
  });
514
669
  }
515
670
  // src/primitives/overlay.tsx
671
+ const modalSizes = {
672
+ sm: "max-w-sm",
673
+ md: "max-w-lg",
674
+ lg: "max-w-2xl",
675
+ xl: "max-w-5xl",
676
+ /** Fills the page inside the overlay's padding. */
677
+ full: "h-full max-h-none max-w-none"
678
+ };
516
679
  /** A centered dialog over a dimmed page. Escape and clicking outside close it; focus stays inside. */
517
- function Modal({ isOpen, onOpenChange, title, children, className, ...props }) {
680
+ function Modal({ isOpen, onOpenChange, title, size = "lg", placement = "center", children, className, ...props }) {
518
681
  return /* @__PURE__ */ jsx(ModalOverlay, {
519
682
  isOpen,
520
683
  onOpenChange,
521
684
  isDismissable: true,
522
- className: "fixed inset-0 z-50 flex items-center justify-center bg-(--chat-backdrop) p-4 data-entering:animate-chat-fade-in data-exiting:animate-chat-fade-out",
685
+ className: cx("fixed inset-0 z-50 flex justify-center bg-(--chat-backdrop) p-4 data-entering:animate-chat-fade-in data-exiting:animate-chat-fade-out", placement === "top" ? "items-start pt-[10dvh]" : "items-center"),
523
686
  children: /* @__PURE__ */ jsx(Modal$1, {
524
- className: clsx("flex max-h-[85dvh] w-full max-w-2xl flex-col rounded-chat-lg border border-chat-popover-border bg-chat-popover font-chat-sans text-chat-fg shadow-chat-popover", "data-entering:animate-chat-in data-exiting:animate-chat-out", className),
687
+ className: cx("flex max-h-[85dvh] w-full flex-col rounded-chat-lg border border-chat-popover-border bg-chat-popover font-chat-sans text-chat-fg shadow-chat-popover", modalSizes[size], "data-entering:animate-chat-in data-exiting:animate-chat-out", className),
525
688
  children: /* @__PURE__ */ jsxs(Dialog, {
526
689
  ...props,
527
690
  className: "flex min-h-0 flex-1 flex-col outline-none",
@@ -542,7 +705,7 @@ function Drawer({ isOpen, onOpenChange, side = "left", children, className, ...p
542
705
  isDismissable: true,
543
706
  className: "fixed inset-0 z-40 bg-(--chat-backdrop) data-entering:animate-chat-fade-in data-exiting:animate-chat-fade-out",
544
707
  children: /* @__PURE__ */ jsx(Modal$1, {
545
- className: clsx("fixed inset-y-0 w-[min(20rem,85vw)] bg-chat-sidebar text-chat-fg shadow-chat-popover outline-none", side === "left" ? "left-0 data-entering:animate-chat-drawer-in data-exiting:animate-chat-drawer-out" : "right-0 data-entering:animate-chat-drawer-in-right data-exiting:animate-chat-drawer-out-right", className),
708
+ className: cx("fixed inset-y-0 w-[min(20rem,85vw)] bg-chat-sidebar text-chat-fg shadow-chat-popover outline-none", side === "left" ? "left-0 data-entering:animate-chat-drawer-in data-exiting:animate-chat-drawer-out" : "right-0 data-entering:animate-chat-drawer-in-right data-exiting:animate-chat-drawer-out-right", className),
546
709
  children: /* @__PURE__ */ jsx(Dialog, {
547
710
  ...props,
548
711
  className: "h-full outline-none",
@@ -556,10 +719,10 @@ function Drawer({ isOpen, onOpenChange, side = "left", children, className, ...p
556
719
  * A titled region that fills its container and lets the body scroll: a dock panel, an agent pane in a
557
720
  * grid, a settings card. A `ChatView` placed in the body sizes itself to the panel.
558
721
  */
559
- function Panel({ title, actions, footer, children, variant = "card", className, ...props }) {
722
+ function Panel({ title, actions, footer, children, variant = "card", padded = false, className, ...props }) {
560
723
  return /* @__PURE__ */ jsxs("section", {
561
724
  "aria-label": props["aria-label"],
562
- className: clsx("flex min-h-0 min-w-0 flex-col overflow-hidden bg-chat-bg font-chat-sans text-chat-fg", variant === "card" && "rounded-chat-md border border-chat-border", className),
725
+ className: cx("flex min-h-0 min-w-0 flex-col overflow-hidden bg-chat-bg font-chat-sans text-chat-fg", variant === "card" && "rounded-chat-md border border-chat-border", className),
563
726
  children: [
564
727
  (title || actions) && /* @__PURE__ */ jsxs("header", {
565
728
  className: "flex h-10 shrink-0 items-center gap-2 border-b border-chat-divider px-3 text-(length:--chat-ui-font-size) leading-5",
@@ -572,7 +735,7 @@ function Panel({ title, actions, footer, children, variant = "card", className,
572
735
  })]
573
736
  }),
574
737
  /* @__PURE__ */ jsx("div", {
575
- className: "min-h-0 flex-1 overflow-y-auto [scrollbar-width:thin]",
738
+ className: cx("min-h-0 flex-1 overflow-y-auto [scrollbar-width:thin]", padded && "p-3"),
576
739
  children
577
740
  }),
578
741
  footer && /* @__PURE__ */ jsx("footer", {
@@ -632,8 +795,25 @@ function useReducedMotion() {
632
795
  return useCallback(() => shouldReduceMotion(motion), [motion]);
633
796
  }
634
797
  // src/primitives/highlight.ts
798
+ /**
799
+ * Shiki with its JavaScript regex engine, not the default Oniguruma WebAssembly one, so a page needs no
800
+ * `'wasm-unsafe-eval'` in its Content Security Policy. Languages and themes load on demand.
801
+ */
635
802
  let shiki;
636
- const loadShiki = () => shiki ??= import("shiki");
803
+ const loadShiki = () => shiki ??= (async () => {
804
+ const [lib, { createJavaScriptRegexEngine }] = await Promise.all([import("shiki"), import("shiki/engine/javascript")]);
805
+ return {
806
+ lib,
807
+ highlighter: await lib.createHighlighter({
808
+ themes: [],
809
+ langs: [],
810
+ engine: createJavaScriptRegexEngine({ forgiving: true })
811
+ })
812
+ };
813
+ })().catch((error) => {
814
+ shiki = void 0;
815
+ throw error;
816
+ });
637
817
  const cache = /* @__PURE__ */ new Map();
638
818
  const warned = /* @__PURE__ */ new Set();
639
819
  /** The theme id if Shiki bundles it; otherwise the default, with one console.warn per unknown id. */
@@ -646,6 +826,13 @@ function pick(id, fallback, known) {
646
826
  }
647
827
  return fallback;
648
828
  }
829
+ let failedWarned = false;
830
+ /** One console.warn when highlighting breaks (a blocked chunk, a CSP violation), instead of silently showing plain code. */
831
+ function warnFailed(error) {
832
+ if (failedWarned) return;
833
+ failedWarned = true;
834
+ console.warn("agentic-ui-kit: code highlighting failed, showing plain code.", error);
835
+ }
649
836
  /** Parses "--a:#fff;--b:#000" into an object. */
650
837
  function parseVars(css) {
651
838
  const vars = {};
@@ -659,15 +846,17 @@ async function highlight(code, language, light, dark) {
659
846
  const key = `${light}\n${dark}\n${language}\n${code}`;
660
847
  const hit = cache.get(key);
661
848
  if (hit) return hit;
662
- const { codeToTokens, bundledLanguages, bundledLanguagesAlias, bundledThemes } = await loadShiki();
849
+ const { lib: { bundledLanguages, bundledLanguagesAlias, bundledThemes }, highlighter } = await loadShiki();
663
850
  const lang = language.toLowerCase();
664
851
  if (!(lang in bundledLanguages) && !(lang in bundledLanguagesAlias)) return void 0;
665
- const { tokens, bg, fg } = await codeToTokens(code, {
852
+ const themes = {
853
+ light: pick(light, DEFAULT_CODE_THEMES.light, bundledThemes),
854
+ dark: pick(dark, DEFAULT_CODE_THEMES.dark, bundledThemes)
855
+ };
856
+ await Promise.all([highlighter.loadLanguage(lang), highlighter.loadTheme(themes.light, themes.dark)]);
857
+ const { tokens, bg, fg } = highlighter.codeToTokens(code, {
666
858
  lang,
667
- themes: {
668
- light: pick(light, DEFAULT_CODE_THEMES.light, bundledThemes),
669
- dark: pick(dark, DEFAULT_CODE_THEMES.dark, bundledThemes)
670
- },
859
+ themes,
671
860
  defaultColor: false
672
861
  });
673
862
  const result = {
@@ -692,7 +881,10 @@ function useHighlight(code, language) {
692
881
  useEffect(() => {
693
882
  if (!language) return setResult(void 0);
694
883
  let cancelled = false;
695
- highlight(code, language, light, dark).then((r) => !cancelled && setResult(r), () => !cancelled && setResult(void 0));
884
+ highlight(code, language, light, dark).then((r) => !cancelled && setResult(r), (error) => {
885
+ warnFailed(error);
886
+ if (!cancelled) setResult(void 0);
887
+ });
696
888
  return () => {
697
889
  cancelled = true;
698
890
  };
@@ -726,7 +918,7 @@ function CodeBlock({ code, language, label = language, variant = "plain", copy =
726
918
  }), copy && /* @__PURE__ */ jsx(CopyButton, { text: code })]
727
919
  }), /* @__PURE__ */ jsx("pre", {
728
920
  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]"),
729
- 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", {
921
+ children: /* @__PURE__ */ jsxs("code", { children: [highlighted ? highlighted.map((line, i) => /* @__PURE__ */ jsxs(Fragment, { children: [i > 0 && "\n", line.map((token, j) => /* @__PURE__ */ jsx("span", {
730
922
  className: "chat-code-token",
731
923
  style: token.htmlStyle,
732
924
  children: token.content
@@ -738,12 +930,15 @@ function CodeBlock({ code, language, label = language, variant = "plain", copy =
738
930
  /**
739
931
  * Applies a chat theme to part of a page. Menus, tooltips and dialogs opened inside render into this
740
932
  * element instead of `<body>`, so they get the same theme. An app themed as a whole (variables on
741
- * `:root`) does not need it for colors, but still uses it for `codeThemes` and `motion`.
933
+ * `:root`) does not need it for colors, but still uses it for `codeThemes` and `motion`. The element is
934
+ * `height: 100%` of its parent by default (so `ChatView` has a height without Tailwind); a `className` such
935
+ * as `h-screen`, or a style rule of your own, replaces that.
742
936
  */
743
937
  function ChatTheme({ theme, codeThemes, motion, labels, icons, children, className }) {
744
938
  const portal = useRef(null);
745
939
  const getContainer = useCallback(() => portal.current, []);
746
940
  return /* @__PURE__ */ jsxs("div", {
941
+ "data-chat-root": "",
747
942
  "data-chat-theme": theme,
748
943
  "data-chat-motion": motion,
749
944
  className: clsx("bg-chat-bg font-chat-sans text-chat-fg", className),
@@ -881,7 +1076,7 @@ const DECLINED = "Declined by the user";
881
1076
  * retries. With an `AgentProvider` it also runs the agent loop: client tools run in the page, approvals
882
1077
  * wait for the user, then the reply continues; every finished tool call reaches its tool's `onResult`.
883
1078
  */
884
- function useChat({ transport, conversationId, initialMessages = [], onTitle, agent, maxSteps = 12 }) {
1079
+ function useChat({ transport, conversationId, initialMessages = [], onTitle, agent, maxSteps = 12, resume = false }) {
885
1080
  const contextAgent = useAgentRegistry();
886
1081
  const registry = agent === void 0 ? contextAgent : agent;
887
1082
  const [messages, setMessages] = useState(initialMessages);
@@ -949,7 +1144,7 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
949
1144
  if (ended) commit(ended);
950
1145
  }, [commit]);
951
1146
  const advanceRef = useRef(() => {});
952
- const start = useCallback((history, options, continuation) => {
1147
+ const start = useCallback((history, options, continuation, joining) => {
953
1148
  const message = history.at(-1);
954
1149
  const controller = new AbortController();
955
1150
  const current = {
@@ -965,7 +1160,7 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
965
1160
  try {
966
1161
  const { transport, conversationId, registry } = latest.current;
967
1162
  const tools = registry?.specs();
968
- for await (const event of transport.send({
1163
+ const events = joining && conversationId !== void 0 && transport.resume ? transport.resume(conversationId, controller.signal) : transport.send({
969
1164
  message,
970
1165
  history,
971
1166
  signal: controller.signal,
@@ -975,7 +1170,8 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
975
1170
  ...conversationId !== void 0 && { conversationId },
976
1171
  ...options.model !== void 0 && { model: options.model },
977
1172
  ...options.effort !== void 0 && { effort: options.effort }
978
- })) {
1173
+ });
1174
+ for await (const event of events) {
979
1175
  if (controller.signal.aborted) break;
980
1176
  if (event.type === "title") {
981
1177
  latest.current.onTitle?.(event.title);
@@ -1147,7 +1343,30 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
1147
1343
  };
1148
1344
  start(messages.slice(0, index), options);
1149
1345
  }, [start]);
1150
- useEffect(() => () => run.current?.controller.abort(), []);
1346
+ const joined = useRef(false);
1347
+ useEffect(() => {
1348
+ if (!resume || joined.current || run.current) return;
1349
+ const { transport, conversationId, messages } = latest.current;
1350
+ if (!transport.resume || conversationId === void 0) return;
1351
+ joined.current = true;
1352
+ const last = messages.at(-1);
1353
+ const history = last?.role === "assistant" && last.status === "streaming" ? messages.slice(0, -1) : messages;
1354
+ loop.current = {
1355
+ options: {},
1356
+ step: 0
1357
+ };
1358
+ start(history, {}, void 0, true);
1359
+ }, [resume, start]);
1360
+ const mounted = useRef(false);
1361
+ useEffect(() => {
1362
+ mounted.current = true;
1363
+ return () => {
1364
+ mounted.current = false;
1365
+ queueMicrotask(() => {
1366
+ if (!mounted.current) run.current?.controller.abort();
1367
+ });
1368
+ };
1369
+ }, []);
1151
1370
  return {
1152
1371
  messages,
1153
1372
  busy,
@@ -1160,7 +1379,7 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
1160
1379
  }
1161
1380
  // src/hooks/sse-transport.ts
1162
1381
  /** A `Transport` for backends that stream `StreamEvent`s over server-sent events (see `formatEvent`). */
1163
- function sseTransport({ url, headers, stopUrl, fetch: fetchImpl = fetch }) {
1382
+ function sseTransport({ url, headers, stopUrl, resumeUrl, fetch: fetchImpl = fetch }) {
1164
1383
  const headersFor = async () => ({
1165
1384
  "content-type": "application/json",
1166
1385
  ...Object.fromEntries(new Headers(typeof headers === "function" ? await headers() : headers))
@@ -1176,6 +1395,20 @@ function sseTransport({ url, headers, stopUrl, fetch: fetchImpl = fetch }) {
1176
1395
  if (!response.ok || !response.body) throw new Error(`${url} answered ${response.status} ${response.statusText}`.trim());
1177
1396
  yield* readEventStream(response.body);
1178
1397
  },
1398
+ ...resumeUrl && { async *resume(conversationId, signal) {
1399
+ const target = typeof resumeUrl === "function" ? resumeUrl(conversationId) : `${resumeUrl}${resumeUrl.includes("?") ? "&" : "?"}conversationId=${encodeURIComponent(conversationId)}`;
1400
+ const { "content-type": _, ...headers } = await headersFor();
1401
+ const response = await fetchImpl(target, {
1402
+ method: "GET",
1403
+ headers: {
1404
+ ...headers,
1405
+ accept: "text/event-stream"
1406
+ },
1407
+ signal
1408
+ });
1409
+ if (!response.ok || !response.body) throw new Error(`${target} answered ${response.status} ${response.statusText}`.trim());
1410
+ yield* readEventStream(response.body);
1411
+ } },
1179
1412
  ...stopUrl && { async stop(runId) {
1180
1413
  await fetchImpl(stopUrl, {
1181
1414
  method: "POST",
@@ -1280,6 +1513,30 @@ const SidebarContext = createContext({
1280
1513
  dismiss: () => {}
1281
1514
  });
1282
1515
  const useSidebar = () => useContext(SidebarContext);
1516
+ /** State that the parent may own: `value` makes it controlled, otherwise `initial` seeds it. */
1517
+ function useControllable(value, initial, onChange) {
1518
+ const [inner, setInner] = useState(initial);
1519
+ const current = value ?? inner;
1520
+ const latest = useRef({
1521
+ current,
1522
+ value,
1523
+ onChange
1524
+ });
1525
+ latest.current = {
1526
+ current,
1527
+ value,
1528
+ onChange
1529
+ };
1530
+ return [current, useCallback((next) => {
1531
+ const { current, value, onChange } = latest.current;
1532
+ const resolved = typeof next === "function" ? next(current) : next;
1533
+ if (resolved === current) return;
1534
+ if (value === void 0) setInner(resolved);
1535
+ onChange?.(resolved);
1536
+ }, [])];
1537
+ }
1538
+ /** True on Apple platforms, where the shortcut modifier is ⌘ instead of Ctrl. */
1539
+ const isMacPlatform = () => typeof navigator !== "undefined" && /mac|iphone|ipad|ipod/i.test(navigator.platform || navigator.userAgent);
1283
1540
  // src/components/replacements.tsx
1284
1541
  /**
1285
1542
  * In-place updates of drawn visuals: a tool call whose input has `replaces: <id>` takes the place of that
@@ -2193,7 +2450,7 @@ function Code({ node, className, children }) {
2193
2450
  });
2194
2451
  }
2195
2452
  const components = {
2196
- pre: ({ children }) => /* @__PURE__ */ jsx(Fragment, { children }),
2453
+ pre: ({ children }) => /* @__PURE__ */ jsx(Fragment$1, { children }),
2197
2454
  code: ({ node, className, children }) => /* @__PURE__ */ jsx(Code, {
2198
2455
  node,
2199
2456
  className,
@@ -2201,12 +2458,39 @@ const components = {
2201
2458
  })
2202
2459
  };
2203
2460
  const LinksContext = createContext(void 0);
2204
- function MarkdownLinksProvider({ renderLink, children }) {
2461
+ function MarkdownLinksProvider({ renderLink, renderLinkList, children }) {
2462
+ const value = useMemo(() => ({
2463
+ renderLink,
2464
+ renderLinkList
2465
+ }), [renderLink, renderLinkList]);
2205
2466
  return /* @__PURE__ */ jsx(LinksContext.Provider, {
2206
- value: renderLink,
2467
+ value,
2207
2468
  children
2208
2469
  });
2209
2470
  }
2471
+ /** The rendered link an item consists of: an `a` alone, or alone in its paragraph (a loose list). */
2472
+ function soleLink(children) {
2473
+ const nodes = Children.toArray(children).filter((c) => !(typeof c === "string" && !c.trim()));
2474
+ if (nodes.length !== 1 || !isValidElement(nodes[0])) return void 0;
2475
+ const props = nodes[0].props;
2476
+ if (typeof props.href === "string") return {
2477
+ href: props.href,
2478
+ children: props.children
2479
+ };
2480
+ return props.node?.tagName === "p" ? soleLink(props.children) : void 0;
2481
+ }
2482
+ function listOf(ordered, props, renderLinkList) {
2483
+ const Default = ordered ? defaultComponents.ol : defaultComponents.ul;
2484
+ const { children } = props;
2485
+ const links = Children.toArray(children).filter((c) => isValidElement(c)).map((item) => soleLink(item.props.children));
2486
+ const list = /* @__PURE__ */ jsx(Default, { ...props });
2487
+ if (links.length === 0 || links.some((l) => !l)) return list;
2488
+ return renderLinkList({
2489
+ ordered,
2490
+ links,
2491
+ children: list
2492
+ }) ?? list;
2493
+ }
2210
2494
  /** Message markdown: GFM, KaTeX math ($…$ and $$…$$), Shiki code blocks, smooth fade-in while streaming. */
2211
2495
  function Markdown({ children, streaming = false, className, partId }) {
2212
2496
  const text = useSmoothText(children, streaming);
@@ -2237,22 +2521,26 @@ function BlockScope({ id, streaming, children }) {
2237
2521
  });
2238
2522
  }
2239
2523
  const MarkdownBody = memo(function MarkdownBody({ text, animating, fade }) {
2240
- const renderLink = useContext(LinksContext);
2241
- const withLinks = useMemo(() => renderLink ? {
2524
+ const { renderLink, renderLinkList } = useContext(LinksContext) ?? {};
2525
+ const withLinks = useMemo(() => ({
2242
2526
  ...components,
2243
- a: ({ href, children }) => {
2527
+ ...renderLink && { a: ({ href, children }) => {
2244
2528
  const custom = href ? renderLink({
2245
2529
  href,
2246
2530
  children
2247
2531
  }) : void 0;
2248
- return custom !== void 0 ? /* @__PURE__ */ jsx(Fragment, { children: custom }) : /* @__PURE__ */ jsx("a", {
2532
+ return custom !== void 0 ? /* @__PURE__ */ jsx(Fragment$1, { children: custom }) : /* @__PURE__ */ jsx("a", {
2249
2533
  href,
2250
2534
  target: "_blank",
2251
2535
  rel: "noopener noreferrer",
2252
2536
  children
2253
2537
  });
2538
+ } },
2539
+ ...renderLinkList && {
2540
+ ul: (props) => listOf(false, props, renderLinkList),
2541
+ ol: (props) => listOf(true, props, renderLinkList)
2254
2542
  }
2255
- } : components, [renderLink]);
2543
+ }), [renderLink, renderLinkList]);
2256
2544
  return /* @__PURE__ */ jsx(Streamdown, {
2257
2545
  prefix: PREFIX,
2258
2546
  mode: animating || fade ? "streaming" : "static",
@@ -2312,7 +2600,7 @@ function activityLabel(part, labels, view) {
2312
2600
  */
2313
2601
  function activityHeading(parts, views, labels) {
2314
2602
  const waiting = parts.find((p) => p.type === "tool_call" && p.status === "awaiting");
2315
- if (waiting) return /* @__PURE__ */ jsxs(Fragment, { children: [activityLabel(waiting, labels, views[waiting.name]), labels.waitingForYou] });
2603
+ if (waiting) return /* @__PURE__ */ jsxs(Fragment$1, { children: [activityLabel(waiting, labels, views[waiting.name]), labels.waitingForYou] });
2316
2604
  if (parts.some(isWorking)) {
2317
2605
  const last = parts.at(-1);
2318
2606
  return activityLabel(last, labels, last.type === "tool_call" ? views[last.name] : void 0);
@@ -2331,6 +2619,7 @@ function stringOf(value) {
2331
2619
  }
2332
2620
  /** A shell command. Input `{ command, description? }`; result a string or `{ stdout, stderr, exitCode }`. */
2333
2621
  const commandTool = {
2622
+ ask: (_, labels) => labels.runCommandAsk,
2334
2623
  label: (_, labels) => labels.ranCommandLabel,
2335
2624
  hint: (part) => stringOf(recordOf(part.input).description),
2336
2625
  summary: (n, labels) => labels.ranCommands(n),
@@ -2363,6 +2652,7 @@ const commandTool = {
2363
2652
  * part's display `data`, else its `result`.
2364
2653
  */
2365
2654
  const webSearchTool = {
2655
+ ask: (_, labels) => labels.searchWebAsk,
2366
2656
  label: (_, labels) => labels.searchedWebLabel,
2367
2657
  subtitle: (part) => stringOf(recordOf(part.input).query) ?? part.subtitle,
2368
2658
  summary: (n, labels) => labels.searchedWeb(n),
@@ -2410,6 +2700,7 @@ const webSearchTool = {
2410
2700
  };
2411
2701
  /** Reads a web page. Input `{ url }`; the row shows the page's host, the detail links to it. */
2412
2702
  const webFetchTool = {
2703
+ ask: (_, labels) => labels.readPageAsk,
2413
2704
  label: (_, labels) => labels.readPageLabel,
2414
2705
  subtitle: (part) => {
2415
2706
  const url = stringOf(recordOf(part.input).url);
@@ -2550,7 +2841,7 @@ function ActivityGroup({ parts, defaultExpanded = false, className }) {
2550
2841
  }) : /* @__PURE__ */ jsx(Disclosure, {
2551
2842
  isExpanded: expanded,
2552
2843
  onExpandedChange: setExpanded,
2553
- summary: /* @__PURE__ */ jsxs(Fragment, { children: [
2844
+ summary: /* @__PURE__ */ jsxs(Fragment$1, { children: [
2554
2845
  /* @__PURE__ */ jsx(Lead, { live }),
2555
2846
  /* @__PURE__ */ jsx(Live, {
2556
2847
  live,
@@ -2584,7 +2875,7 @@ const SHIMMER_MS = 2e3;
2584
2875
  * shared clock, so every live label sweeps in step and a remount does not restart it.
2585
2876
  */
2586
2877
  function Live({ live, children }) {
2587
- return live ? /* @__PURE__ */ jsx(Shimmer, { children }) : /* @__PURE__ */ jsx(Fragment, { children });
2878
+ return live ? /* @__PURE__ */ jsx(Shimmer, { children }) : /* @__PURE__ */ jsx(Fragment$1, { children });
2588
2879
  }
2589
2880
  function Shimmer({ children }) {
2590
2881
  const [delay] = useState(() => `-${Math.round(performance.now() % SHIMMER_MS)}ms`);
@@ -2602,7 +2893,7 @@ function SingleStep({ part, live, after, expanded, onExpandedChange }) {
2602
2893
  return /* @__PURE__ */ jsx(Disclosure, {
2603
2894
  isExpanded: expanded,
2604
2895
  onExpandedChange,
2605
- summary: /* @__PURE__ */ jsxs(Fragment, { children: [
2896
+ summary: /* @__PURE__ */ jsxs(Fragment$1, { children: [
2606
2897
  /* @__PURE__ */ jsx(Lead, { live }),
2607
2898
  /* @__PURE__ */ jsx(Live, {
2608
2899
  live,
@@ -2773,11 +3064,19 @@ function useAttachments({ upload } = {}) {
2773
3064
  toParts
2774
3065
  };
2775
3066
  }
3067
+ const IMAGE_EXTENSION = /\.(png|jpe?g|gif|webp|avif|svg|bmp|ico)$/i;
3068
+ /** Whether the card draws a thumbnail: by mime when given, else by `kind`, else by the file extension of the url or name. */
3069
+ function isImage({ mime, kind, url, name }) {
3070
+ if (mime) return mime.startsWith("image/");
3071
+ if (kind) return kind === "image";
3072
+ const path = url && !url.startsWith("data:") ? url.split(/[?#]/)[0] : "";
3073
+ return url?.startsWith("data:image/") === true || IMAGE_EXTENSION.test(path) || IMAGE_EXTENSION.test(name);
3074
+ }
2776
3075
  const extension = (name) => /\.([a-z0-9]{1,5})$/i.exec(name)?.[1]?.toUpperCase();
2777
3076
  /** A square card: pasted text preview with a PASTED badge, an image thumbnail, or a file with its type. */
2778
3077
  function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", className }) {
2779
3078
  const icons = useIcons();
2780
- const image = a.mime.startsWith("image/") && a.url;
3079
+ const image = a.url && isImage(a);
2781
3080
  const pasted = a.text !== void 0;
2782
3081
  const labels = useLabels();
2783
3082
  const uploading = a.status === "uploading";
@@ -2842,7 +3141,7 @@ function AttachmentRow({ attachments, onRemove, onEditText }) {
2842
3141
  const [editing, setEditing] = useState();
2843
3142
  const labels = useLabels();
2844
3143
  if (attachments.length === 0) return null;
2845
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("ul", {
3144
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("ul", {
2846
3145
  "aria-label": labels.attachments,
2847
3146
  className: "flex gap-2.5 overflow-x-auto px-0.5 pt-2.5 pb-1 [scrollbar-width:thin]",
2848
3147
  children: attachments.map((a) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(AttachmentCard, {
@@ -3012,7 +3311,7 @@ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPar
3012
3311
  if (block.kind === "activity") {
3013
3312
  const cards = block.parts.filter((p) => p.type === "tool_call" && views[p.name]?.Card);
3014
3313
  if (!cards.length) return /* @__PURE__ */ jsx(ActivityGroup, { parts: block.parts }, block.id);
3015
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ActivityGroup, { parts: block.parts }), cards.map((p) => {
3314
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ActivityGroup, { parts: block.parts }), cards.map((p) => {
3016
3315
  if (p.type !== "tool_call") return null;
3017
3316
  const Card = views[p.name].Card;
3018
3317
  return /* @__PURE__ */ jsx(Card, { part: p }, p.id);
@@ -3054,7 +3353,7 @@ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPar
3054
3353
  onPress: onReadAloud,
3055
3354
  children: icons.speak
3056
3355
  }),
3057
- onFeedback && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(IconButton, {
3356
+ onFeedback && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(IconButton, {
3058
3357
  label: labels.goodResponse,
3059
3358
  size: "sm",
3060
3359
  onPress: () => onFeedback("good"),
@@ -3108,7 +3407,7 @@ function ApprovalCard({ approval, onRespond, className }) {
3108
3407
  children: icons.approval
3109
3408
  }), /* @__PURE__ */ jsx("span", {
3110
3409
  className: "min-w-0 flex-1 font-medium",
3111
- children: view.ask?.(part) ?? labels.allowAction(activityLabel(part, labels, view))
3410
+ children: view.ask?.(part, labels) ?? labels.allowAction(view.askLabel?.(part, labels) ?? activityLabel(part, labels, view))
3112
3411
  })]
3113
3412
  }),
3114
3413
  /* @__PURE__ */ jsx("div", {
@@ -3139,6 +3438,16 @@ function ApprovalCard({ approval, onRespond, className }) {
3139
3438
  // src/components/context-panel.tsx
3140
3439
  const FREE_COLOR = "var(--chat-raised)";
3141
3440
  const DEFAULT_COLOR = "var(--chat-subtle)";
3441
+ /** Tokens used (reserved and deferred categories excluded) and tokens still free. */
3442
+ function contextTotals(usage) {
3443
+ const counted = usage.categories.filter((c) => !c.deferred);
3444
+ const used = counted.reduce((sum, c) => sum + (c.reserved ? 0 : c.tokens), 0);
3445
+ const reserved = counted.reduce((sum, c) => sum + (c.reserved ? c.tokens : 0), 0);
3446
+ return {
3447
+ used,
3448
+ free: Math.max(usage.limit - used - reserved, 0)
3449
+ };
3450
+ }
3142
3451
  /**
3143
3452
  * What fills the context window: a usage line, a segmented bar, one row per category with its share, and
3144
3453
  * expandable lists (each MCP tool, each memory file). Deferred categories are listed but not counted.
@@ -3147,9 +3456,7 @@ function ContextPanel({ usage, title, defaultExpanded = true, className }) {
3147
3456
  const labels = useLabels();
3148
3457
  const counted = usage.categories.filter((c) => !c.deferred);
3149
3458
  const deferred = usage.categories.filter((c) => c.deferred);
3150
- const used = counted.reduce((sum, c) => sum + (c.reserved ? 0 : c.tokens), 0);
3151
- const reserved = counted.reduce((sum, c) => sum + (c.reserved ? c.tokens : 0), 0);
3152
- const free = Math.max(usage.limit - used - reserved, 0);
3459
+ const { used, free } = contextTotals(usage);
3153
3460
  const share = (tokens) => formatShare(tokens / usage.limit);
3154
3461
  return /* @__PURE__ */ jsxs(Disclosure, {
3155
3462
  summary: title ?? labels.context,
@@ -3274,6 +3581,121 @@ function GroupList({ group }) {
3274
3581
  function formatShare(fraction) {
3275
3582
  return `${(fraction * 100).toFixed(1)}%`;
3276
3583
  }
3584
+ // src/components/context-meter.tsx
3585
+ const SIZE = 16;
3586
+ const STROKE = 2;
3587
+ const RADIUS = 7;
3588
+ const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
3589
+ /**
3590
+ * A small progress ring for the composer toolbar, filled to the share of the context window in use (accent,
3591
+ * then warning, then danger). Hover for the numbers; click or Enter opens the ContextPanel above it.
3592
+ */
3593
+ function ContextMeter({ usage, tooltip, tooltipNote, isLoading = false, warnAt = .75, dangerAt = .9, placement = "top end", className }) {
3594
+ const labels = useLabels();
3595
+ const { used } = contextTotals(usage);
3596
+ const fraction = usage.limit > 0 ? Math.min(used / usage.limit, 1) : 0;
3597
+ const summary = isLoading ? labels.contextLoading : labels.contextSummary(formatTokens(used), formatTokens(usage.limit), Math.round(fraction * 100));
3598
+ const color = fraction >= dangerAt ? "text-chat-danger" : fraction >= warnAt ? "text-chat-warning" : "text-chat-accent";
3599
+ const button = /* @__PURE__ */ jsx(Button, {
3600
+ variant: "ghost",
3601
+ size: "sm",
3602
+ "aria-label": summary,
3603
+ isDisabled: isLoading,
3604
+ className: clsx("size-7 justify-center px-0 text-chat-muted", isLoading && "data-disabled:opacity-100", className),
3605
+ children: /* @__PURE__ */ jsxs("svg", {
3606
+ "aria-hidden": true,
3607
+ viewBox: `0 0 ${SIZE} ${SIZE}`,
3608
+ className: clsx("size-4", isLoading && "motion-safe:animate-spin"),
3609
+ fill: "none",
3610
+ strokeWidth: STROKE,
3611
+ children: [/* @__PURE__ */ jsx("circle", {
3612
+ cx: SIZE / 2,
3613
+ cy: SIZE / 2,
3614
+ r: RADIUS,
3615
+ stroke: "var(--chat-raised)"
3616
+ }), /* @__PURE__ */ jsx("circle", {
3617
+ cx: SIZE / 2,
3618
+ cy: SIZE / 2,
3619
+ r: RADIUS,
3620
+ stroke: "currentColor",
3621
+ strokeLinecap: "round",
3622
+ strokeDasharray: CIRCUMFERENCE,
3623
+ strokeDashoffset: CIRCUMFERENCE * (1 - (isLoading ? .25 : fraction)),
3624
+ transform: `rotate(-90 ${SIZE / 2} ${SIZE / 2})`,
3625
+ className: clsx(color, "transition-[stroke-dashoffset]")
3626
+ })]
3627
+ })
3628
+ });
3629
+ return /* @__PURE__ */ jsxs(DialogTrigger$1, { children: [/* @__PURE__ */ jsx(Tooltip, {
3630
+ content: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", { children: tooltip ?? summary }), tooltipNote && /* @__PURE__ */ jsx("div", {
3631
+ className: "text-chat-muted",
3632
+ children: tooltipNote
3633
+ })] }),
3634
+ children: button
3635
+ }), /* @__PURE__ */ jsx(Popover, {
3636
+ placement,
3637
+ className: "w-80 overflow-auto p-3",
3638
+ children: /* @__PURE__ */ jsx(Dialog, {
3639
+ "aria-label": labels.context,
3640
+ className: "outline-none",
3641
+ children: /* @__PURE__ */ jsx(ContextPanel, { usage })
3642
+ })
3643
+ })] });
3644
+ }
3645
+ // src/components/copy-field.tsx
3646
+ /** How long "Copied" stays before the button reads "Copy" again. */
3647
+ const COPIED_MS = 2e3;
3648
+ /**
3649
+ * A read-only, selectable field with a Copy button, for share links. The button says "Copied" for about two
3650
+ * seconds (and announces it to screen readers). Where the clipboard is unavailable or refuses, it selects the
3651
+ * text instead so the reader can copy it by hand.
3652
+ */
3653
+ function CopyField({ value, "aria-label": ariaLabel, onCopy, className }) {
3654
+ const icons = useIcons();
3655
+ const labels = useLabels();
3656
+ const inputRef = useRef(null);
3657
+ const timer = useRef(void 0);
3658
+ const [copied, setCopied] = useState(false);
3659
+ useEffect(() => () => clearTimeout(timer.current), []);
3660
+ const copy = async () => {
3661
+ try {
3662
+ if (!navigator.clipboard) throw new Error("no clipboard");
3663
+ await navigator.clipboard.writeText(value);
3664
+ } catch {
3665
+ inputRef.current?.focus();
3666
+ inputRef.current?.select();
3667
+ return;
3668
+ }
3669
+ setCopied(true);
3670
+ onCopy?.();
3671
+ clearTimeout(timer.current);
3672
+ timer.current = setTimeout(() => setCopied(false), COPIED_MS);
3673
+ };
3674
+ return /* @__PURE__ */ jsxs("div", {
3675
+ className: clsx("flex items-center gap-2 font-chat-sans", className),
3676
+ children: [
3677
+ /* @__PURE__ */ jsx("input", {
3678
+ ref: inputRef,
3679
+ readOnly: true,
3680
+ value,
3681
+ "aria-label": ariaLabel ?? labels.copyFieldLabel,
3682
+ onFocus: (e) => e.currentTarget.select(),
3683
+ className: "h-9 min-w-0 flex-1 rounded-chat-md border border-chat-border bg-chat-bg px-3 text-(length:--chat-ui-font-size) text-chat-fg outline-none focus:border-chat-border-strong"
3684
+ }),
3685
+ /* @__PURE__ */ jsxs(Button, {
3686
+ variant: "subtle",
3687
+ size: "lg",
3688
+ onPress: () => void copy(),
3689
+ children: [/* @__PURE__ */ jsx(IconSlot, { children: copied ? icons.copied : icons.copy }), copied ? labels.copied : labels.copy]
3690
+ }),
3691
+ /* @__PURE__ */ jsx("span", {
3692
+ role: "status",
3693
+ className: "sr-only",
3694
+ children: copied ? labels.copied : ""
3695
+ })
3696
+ ]
3697
+ });
3698
+ }
3277
3699
  // src/components/dictation.tsx
3278
3700
  /**
3279
3701
  * Voice dictation in the composer, similar to claude.ai's: while listening the words fill the box as they are
@@ -3773,7 +4195,7 @@ function EffortSlider({ efforts, value, onChange, label, className }) {
3773
4195
  function EffortButton({ icon, children, className, ...slider }) {
3774
4196
  const current = slider.efforts.find((e) => e.id === slider.value) ?? slider.efforts[0];
3775
4197
  const labels = useLabels();
3776
- return /* @__PURE__ */ jsxs(DialogTrigger, { children: [/* @__PURE__ */ jsxs(Button, {
4198
+ return /* @__PURE__ */ jsxs(DialogTrigger$1, { children: [/* @__PURE__ */ jsxs(Button, {
3777
4199
  variant: "ghost",
3778
4200
  size: "sm",
3779
4201
  className: clsx("text-chat-muted data-hovered:text-chat-fg", className),
@@ -3801,6 +4223,7 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
3801
4223
  const more = models.filter((m) => m.secondary);
3802
4224
  const item = (m) => /* @__PURE__ */ jsx(MenuItem, {
3803
4225
  id: m.id,
4226
+ ...m.icon && { icon: m.icon },
3804
4227
  description: m.disabledReason ?? m.description,
3805
4228
  isDisabled: m.disabled ?? false,
3806
4229
  children: m.label
@@ -3815,6 +4238,10 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
3815
4238
  "aria-label": labels.modelButton(current?.label ?? model),
3816
4239
  className: clsx("text-chat-muted data-hovered:text-chat-fg", className),
3817
4240
  children: [
4241
+ current?.icon && /* @__PURE__ */ jsx(IconSlot, {
4242
+ className: "text-chat-fg [&_svg]:size-3.5",
4243
+ children: current.icon
4244
+ }),
3818
4245
  /* @__PURE__ */ jsx("span", {
3819
4246
  className: "min-w-0 truncate text-chat-fg",
3820
4247
  children: current?.label ?? model
@@ -3918,10 +4345,12 @@ function MessageList({ messages, onRetry, onEdit, onFeedback, onReadAloud, actio
3918
4345
  behavior
3919
4346
  });
3920
4347
  }, []);
3921
- const onScroll = () => {
4348
+ const nearBottom = () => {
3922
4349
  const el = scroller.current;
3923
- if (!el) return;
3924
- const bottom = el.scrollHeight - el.scrollTop - el.clientHeight < STICK_SLOP;
4350
+ return !el || el.scrollHeight - el.scrollTop - el.clientHeight < STICK_SLOP;
4351
+ };
4352
+ const onScroll = () => {
4353
+ const bottom = nearBottom();
3925
4354
  stuck.current = bottom;
3926
4355
  setAtBottom(bottom);
3927
4356
  };
@@ -3938,7 +4367,7 @@ function MessageList({ messages, onRetry, onEdit, onFeedback, onReadAloud, actio
3938
4367
  const jumpedFor = useRef(start === "top" ? lastUser : null);
3939
4368
  useLayoutEffect(() => {
3940
4369
  if (lastUser === jumpedFor.current) {
3941
- onScroll();
4370
+ setAtBottom(nearBottom());
3942
4371
  return;
3943
4372
  }
3944
4373
  jumpedFor.current = lastUser;
@@ -4084,7 +4513,7 @@ function ChatTitleMenu({ pinned, onPin, onRename, projects, onAddToProject, onCr
4084
4513
  children: labels.shareChat
4085
4514
  }),
4086
4515
  children,
4087
- onDelete && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MenuSeparator, {}), /* @__PURE__ */ jsx(MenuItem, {
4516
+ onDelete && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(MenuSeparator, {}), /* @__PURE__ */ jsx(MenuItem, {
4088
4517
  id: "delete",
4089
4518
  icon: icons.delete,
4090
4519
  shortcut: "D",
@@ -4176,6 +4605,34 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
4176
4605
  }, section.id))
4177
4606
  });
4178
4607
  }
4608
+ /**
4609
+ * An icon button that opens a `ChatList` in a popover, for `Sidebar`'s `railActions`: the list itself is
4610
+ * hidden on the collapsed rail, so this keeps recent chats reachable. Picking a chat closes the popover.
4611
+ */
4612
+ function ChatListRailButton({ label, popoverProps, onSelect, className, ...list }) {
4613
+ const icons = useIcons();
4614
+ const labels = useLabels();
4615
+ const name = label ?? labels.chats;
4616
+ return /* @__PURE__ */ jsxs(DialogTrigger, { children: [/* @__PURE__ */ jsx(IconButton, {
4617
+ label: name,
4618
+ children: icons.chatList
4619
+ }), /* @__PURE__ */ jsx(Popover, {
4620
+ placement: "right top",
4621
+ ...popoverProps,
4622
+ children: /* @__PURE__ */ jsx(Dialog, {
4623
+ "aria-label": name,
4624
+ className: "max-h-[70dvh] w-64 overflow-y-auto p-2 outline-none [scrollbar-width:thin]",
4625
+ children: ({ close }) => /* @__PURE__ */ jsx(ChatList, {
4626
+ ...list,
4627
+ className,
4628
+ onSelect: (id) => {
4629
+ onSelect?.(id);
4630
+ close();
4631
+ }
4632
+ })
4633
+ })
4634
+ })] });
4635
+ }
4179
4636
  function ChatRow({ chat, active, nested, onSelect, menu }) {
4180
4637
  const icons = useIcons();
4181
4638
  const [menuOpen, setMenuOpen] = useState(false);
@@ -4186,15 +4643,22 @@ function ChatRow({ chat, active, nested, onSelect, menu }) {
4186
4643
  role: "img",
4187
4644
  "aria-label": labels.running,
4188
4645
  className: clsx(markerClass, "animate-pulse bg-chat-accent motion-reduce:animate-none")
4189
- }) : chat.unread ? /* @__PURE__ */ jsx("span", {
4646
+ }) : chat.unread && !chat.muted ? /* @__PURE__ */ jsx("span", {
4190
4647
  role: "img",
4191
4648
  "aria-label": labels.unread,
4192
4649
  className: clsx(markerClass, "bg-chat-check")
4193
4650
  }) : null;
4194
- const content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
4195
- className: clsx("min-w-0 flex-1 truncate", chat.unread && !active && "font-medium"),
4196
- children: chat.title
4197
- }), marker] });
4651
+ const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [
4652
+ /* @__PURE__ */ jsx("span", {
4653
+ className: clsx("min-w-0 flex-1 truncate", chat.unread && !active && !chat.muted && "font-medium", chat.muted && "text-chat-muted"),
4654
+ children: chat.title
4655
+ }),
4656
+ chat.detail != null && /* @__PURE__ */ jsx("span", {
4657
+ className: "shrink-0 text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle tabular-nums",
4658
+ children: chat.detail
4659
+ }),
4660
+ marker
4661
+ ] });
4198
4662
  const common = {
4199
4663
  className: clsx(rowClass, nested && "pl-8", active && "bg-chat-selected", menu && (menuOpen || active ? "pr-8" : "group-hover/item:pr-8 group-focus-within/item:pr-8"), menu && "[@media(hover:none)]:pr-10"),
4200
4664
  "aria-current": active ? "page" : void 0,
@@ -4231,7 +4695,7 @@ function ProjectRow({ project, activeId, row }) {
4231
4695
  const holdsActive = project.chats.some((c) => c.id === activeId);
4232
4696
  return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Disclosure$1, {
4233
4697
  defaultExpanded: project.defaultExpanded ?? holdsActive,
4234
- children: ({ isExpanded }) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Heading, {
4698
+ children: ({ isExpanded }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Heading, {
4235
4699
  className: "m-0 font-normal",
4236
4700
  children: /* @__PURE__ */ jsxs(Button$1, {
4237
4701
  slot: "trigger",
@@ -4290,6 +4754,7 @@ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove
4290
4754
  const labels = useLabels();
4291
4755
  const pending = onApprove && approvals?.[0];
4292
4756
  const box = (variant) => {
4757
+ if (composer === false) return null;
4293
4758
  const element = /* @__PURE__ */ jsx(Composer, {
4294
4759
  variant: density === "inline" || short && !empty ? "compact" : variant,
4295
4760
  busy,
@@ -4423,41 +4888,84 @@ function ChatTranscript({ messages, renderPart, widgets, header, notice, footer,
4423
4888
  });
4424
4889
  }
4425
4890
  // src/views/app-shell.tsx
4891
+ const DEFAULT_WIDTH = 288;
4892
+ const KEY_STEP = 16;
4893
+ const clampWidth = (px, min, max) => Math.min(max, Math.max(min, Math.round(px)));
4894
+ function readStoredWidth(key) {
4895
+ if (!key) return void 0;
4896
+ try {
4897
+ const n = Number(window.localStorage.getItem(key));
4898
+ return n > 0 ? n : void 0;
4899
+ } catch {
4900
+ return;
4901
+ }
4902
+ }
4426
4903
  /**
4427
4904
  * A page with a sidebar: pinned and collapsible to an icon rail from tablet width up, a drawer over the
4428
4905
  * content on phones. Children get `useSidebar()` and `<SidebarToggle>`.
4429
4906
  */
4430
- function AppShell({ sidebar, children, defaultCollapsed, className }) {
4907
+ function AppShell({ sidebar, children, defaultCollapsed, collapsed: collapsedProp, onCollapsedChange, scroll = "inner", resizable, width: widthProp, defaultWidth, onWidthChange, persistKey, shortcut = true, className }) {
4431
4908
  const drawer = useMediaQuery(DRAWER_QUERY);
4432
- const [collapsed, setCollapsed] = useState(() => defaultCollapsed ?? (typeof window !== "undefined" && window.matchMedia("(max-width: 1023.98px)").matches));
4909
+ const labels = useLabels();
4910
+ const [collapsed, setCollapsed] = useControllable(collapsedProp, () => defaultCollapsed ?? (typeof window !== "undefined" && window.matchMedia("(max-width: 1023.98px)").matches), onCollapsedChange);
4433
4911
  const [drawerOpen, setDrawerOpen] = useState(false);
4434
- const toggle = useCallback(() => drawer ? setDrawerOpen((o) => !o) : setCollapsed((c) => !c), [drawer]);
4912
+ const toggle = useCallback(() => drawer ? setDrawerOpen((o) => !o) : setCollapsed((c) => !c), [drawer, setCollapsed]);
4435
4913
  const dismiss = useCallback(() => setDrawerOpen(false), []);
4914
+ const min = resizable?.min ?? 0;
4915
+ const max = resizable?.max ?? Infinity;
4916
+ const [rawWidth, setWidth] = useControllable(widthProp, () => readStoredWidth(persistKey) ?? defaultWidth ?? DEFAULT_WIDTH, (px) => {
4917
+ if (persistKey) try {
4918
+ window.localStorage.setItem(persistKey, String(px));
4919
+ } catch {}
4920
+ onWidthChange?.(px);
4921
+ });
4922
+ const width = resizable ? clampWidth(rawWidth, min, max) : void 0;
4923
+ const shortcutKeys = shortcut ? `${isMacPlatform() ? "⌘" : labels.ctrlKey} B` : void 0;
4924
+ useEffect(() => {
4925
+ if (!shortcut) return;
4926
+ const mac = isMacPlatform();
4927
+ const onKey = (e) => {
4928
+ if (e.defaultPrevented || e.altKey || e.shiftKey || e.key.toLowerCase() !== "b") return;
4929
+ if (mac ? !e.metaKey || e.ctrlKey : !e.ctrlKey || e.metaKey) return;
4930
+ e.preventDefault();
4931
+ toggle();
4932
+ };
4933
+ window.addEventListener("keydown", onKey);
4934
+ return () => window.removeEventListener("keydown", onKey);
4935
+ }, [shortcut, toggle]);
4436
4936
  const pinned = useMemo(() => ({
4437
4937
  collapsed,
4438
4938
  drawer,
4439
4939
  drawerOpen,
4440
4940
  toggle,
4441
- dismiss
4941
+ dismiss,
4942
+ ...shortcutKeys && { shortcut: shortcutKeys }
4442
4943
  }), [
4443
4944
  collapsed,
4444
4945
  drawer,
4445
4946
  drawerOpen,
4446
4947
  toggle,
4447
- dismiss
4948
+ dismiss,
4949
+ shortcutKeys
4448
4950
  ]);
4449
- const labels = useLabels();
4450
4951
  const inDrawer = useMemo(() => ({
4451
4952
  ...pinned,
4452
4953
  collapsed: false
4453
4954
  }), [pinned]);
4955
+ const windowScroll = scroll === "window";
4454
4956
  return /* @__PURE__ */ jsxs(SidebarContext.Provider, {
4455
4957
  value: pinned,
4456
4958
  children: [/* @__PURE__ */ jsxs("div", {
4457
- className: clsx("flex h-dvh overflow-hidden bg-chat-bg text-chat-fg", className),
4458
- children: [!drawer && /* @__PURE__ */ jsx("aside", {
4459
- className: clsx("shrink-0 overflow-hidden border-r border-chat-divider bg-chat-sidebar transition-[width] duration-200 ease-out motion-reduce:transition-none", collapsed ? "w-13" : "w-(--chat-sidebar-width,18rem)"),
4460
- children: sidebar
4959
+ className: clsx("flex bg-chat-bg text-chat-fg", windowScroll ? "min-h-dvh" : "h-dvh overflow-hidden", className),
4960
+ children: [!drawer && /* @__PURE__ */ jsxs("aside", {
4961
+ style: width ? { "--chat-sidebar-width": `${width}px` } : void 0,
4962
+ className: clsx("relative shrink-0 border-r border-chat-divider bg-chat-sidebar transition-[width] duration-200 ease-out motion-reduce:transition-none data-resizing:transition-none", windowScroll ? "sticky top-0 h-dvh self-start overflow-clip" : "overflow-hidden", collapsed ? "w-13" : "w-(--chat-sidebar-width,18rem)"),
4963
+ children: [sidebar, resizable && !collapsed && width && /* @__PURE__ */ jsx(ResizeHandle, {
4964
+ width,
4965
+ min,
4966
+ max,
4967
+ onCommit: setWidth
4968
+ })]
4461
4969
  }), /* @__PURE__ */ jsx("main", {
4462
4970
  className: "flex min-w-0 flex-1 flex-col",
4463
4971
  children
@@ -4473,15 +4981,58 @@ function AppShell({ sidebar, children, defaultCollapsed, className }) {
4473
4981
  })]
4474
4982
  });
4475
4983
  }
4476
- /** Opens the drawer on phones, collapses / expands the pinned sidebar elsewhere. */
4984
+ /** The draggable right edge of the sidebar. A drag moves the aside's width variable alone and commits on release. */
4985
+ function ResizeHandle({ width, min, max, onCommit }) {
4986
+ const labels = useLabels();
4987
+ const live = useRef(null);
4988
+ const aside = (el) => el.closest("aside");
4989
+ const move = (el, px) => {
4990
+ live.current = clampWidth(px, min, max);
4991
+ aside(el).style.setProperty("--chat-sidebar-width", `${live.current}px`);
4992
+ };
4993
+ const end = (el) => {
4994
+ delete aside(el).dataset.resizing;
4995
+ if (live.current != null) onCommit(live.current);
4996
+ live.current = null;
4997
+ };
4998
+ return /* @__PURE__ */ jsx("div", {
4999
+ role: "separator",
5000
+ "aria-orientation": "vertical",
5001
+ "aria-label": labels.resizeSidebar,
5002
+ "aria-valuenow": width,
5003
+ "aria-valuemin": min,
5004
+ "aria-valuemax": max,
5005
+ tabIndex: 0,
5006
+ className: "group/handle absolute inset-y-0 right-0 z-10 w-1.5 cursor-col-resize touch-none outline-none",
5007
+ onPointerDown: (e) => {
5008
+ e.currentTarget.setPointerCapture(e.pointerId);
5009
+ aside(e.currentTarget).dataset.resizing = "";
5010
+ live.current = null;
5011
+ },
5012
+ onPointerMove: (e) => {
5013
+ if (e.currentTarget.hasPointerCapture(e.pointerId)) move(e.currentTarget, e.clientX - aside(e.currentTarget).getBoundingClientRect().left);
5014
+ },
5015
+ onPointerUp: (e) => end(e.currentTarget),
5016
+ onPointerCancel: (e) => end(e.currentTarget),
5017
+ onKeyDown: (e) => {
5018
+ const step = e.shiftKey ? 64 : KEY_STEP;
5019
+ const next = e.key === "ArrowLeft" ? width - step : e.key === "ArrowRight" ? width + step : e.key === "Home" ? min : e.key === "End" ? max : null;
5020
+ if (next == null) return;
5021
+ e.preventDefault();
5022
+ onCommit(clampWidth(next, min, max));
5023
+ },
5024
+ children: /* @__PURE__ */ jsx("span", { className: "absolute inset-y-0 right-0 w-px bg-transparent transition-colors group-hover/handle:bg-chat-border-strong group-focus-visible/handle:w-0.5 group-focus-visible/handle:bg-chat-focus in-data-resizing:bg-chat-focus" })
5025
+ });
5026
+ }
5027
+ /** Opens the drawer on phones, collapses / expands the pinned sidebar elsewhere. Its label names the shortcut, e.g. "(⌘ B)". */
4477
5028
  function SidebarToggle({ drawerOnly = false, className }) {
4478
5029
  const icons = useIcons();
4479
- const { drawer, drawerOpen, collapsed, toggle } = useSidebar();
5030
+ const { drawer, drawerOpen, collapsed, toggle, shortcut } = useSidebar();
4480
5031
  const labels = useLabels();
4481
5032
  if (drawerOnly && !drawer) return null;
4482
5033
  const label = drawer ? drawerOpen ? labels.closeSidebar : labels.openSidebar : collapsed ? labels.expandSidebar : labels.collapseSidebar;
4483
5034
  return /* @__PURE__ */ jsx(IconButton, {
4484
- label,
5035
+ label: shortcut ? labels.withShortcut(label, shortcut) : label,
4485
5036
  onPress: toggle,
4486
5037
  className,
4487
5038
  children: icons.sidebar
@@ -4489,7 +5040,7 @@ function SidebarToggle({ drawerOnly = false, className }) {
4489
5040
  }
4490
5041
  // src/views/sidebar.tsx
4491
5042
  /** The claude.ai sidebar frame. Collapses to an icon rail inside `AppShell`; a drawer on phones. */
4492
- function Sidebar({ brand, brandIcon, headerActions, nav, children, footer, className }) {
5043
+ function Sidebar({ brand, brandIcon, headerActions, railActions, nav, children, footer, className }) {
4493
5044
  const { collapsed } = useSidebar();
4494
5045
  return /* @__PURE__ */ jsxs("div", {
4495
5046
  className: clsx("flex h-full flex-col font-chat-sans", collapsed ? "w-13" : "w-full", className),
@@ -4515,8 +5066,12 @@ function Sidebar({ brand, brandIcon, headerActions, nav, children, footer, class
4515
5066
  /* @__PURE__ */ jsx(SidebarToggle, {})
4516
5067
  ]
4517
5068
  }),
4518
- nav && /* @__PURE__ */ jsx("ul", {
4519
- className: clsx("flex shrink-0 flex-col gap-px px-2 pb-2", collapsed && "items-center"),
5069
+ collapsed && railActions && /* @__PURE__ */ jsx("div", {
5070
+ className: "flex shrink-0 flex-col items-center gap-1 pb-2",
5071
+ children: railActions
5072
+ }),
5073
+ nav && /* @__PURE__ */ jsx(SidebarList, {
5074
+ className: clsx("shrink-0 px-2 pb-2", collapsed && "items-center"),
4520
5075
  children: nav
4521
5076
  }),
4522
5077
  !collapsed && /* @__PURE__ */ jsx("div", {
@@ -4531,32 +5086,65 @@ function Sidebar({ brand, brandIcon, headerActions, nav, children, footer, class
4531
5086
  ]
4532
5087
  });
4533
5088
  }
4534
- /** A nav row: icon and label; just the icon with a tooltip when collapsed. Closes the drawer when picked. */
4535
- function SidebarItem({ icon, label, onPress, href, active, shortcut }) {
5089
+ /** The `<ul>` for `SidebarItem`s outside `Sidebar`'s `nav` slot (e.g. inside a section): no bullets, no padding. */
5090
+ function SidebarList({ children, className }) {
5091
+ return /* @__PURE__ */ jsx("ul", {
5092
+ className: clsx("m-0 flex list-none flex-col gap-px p-0", className),
5093
+ children
5094
+ });
5095
+ }
5096
+ const iconSize = "[&_svg]:size-(--chat-sidebar-icon-size,1rem) [&_svg]:shrink-0";
5097
+ /**
5098
+ * A nav row: icon and label; just the icon with a tooltip when collapsed. Closes the drawer when picked.
5099
+ * Icons are 1rem, or `--chat-sidebar-icon-size`.
5100
+ */
5101
+ function SidebarItem({ icon, label, onPress, href, active, shortcut, badge, description, trailing, title, className: extra, ...rest }) {
4536
5102
  const { collapsed, dismiss } = useSidebar();
4537
- const className = clsx("group flex h-8 cursor-pointer items-center gap-2.5 rounded-chat-sm text-left text-(length:--chat-ui-font-size) leading-5 text-chat-fg outline-none [@media(pointer:coarse)]:h-10", "data-hovered:bg-chat-hover data-pressed:bg-chat-selected data-focus-visible:ring-2 data-focus-visible:ring-chat-focus", "[&_svg]:size-4 [&_svg]:shrink-0", collapsed ? "w-9 justify-center [@media(pointer:coarse)]:w-10" : "w-full px-2", active && "bg-chat-selected");
5103
+ const data = Object.fromEntries(Object.entries(rest).filter(([k]) => k.startsWith("data-")));
5104
+ const hasTrailing = !collapsed && trailing != null;
5105
+ const className = clsx("group flex min-h-8 cursor-pointer items-center gap-2.5 rounded-chat-sm text-left text-(length:--chat-ui-font-size) leading-5 text-chat-fg outline-none [@media(pointer:coarse)]:min-h-10", "data-hovered:bg-chat-hover data-pressed:bg-chat-selected data-focus-visible:ring-2 data-focus-visible:ring-chat-focus", iconSize, collapsed ? "relative w-9 justify-center [@media(pointer:coarse)]:w-10" : "w-full px-2", description && !collapsed && "py-1", hasTrailing && "group-hover/item:pr-9 group-focus-within/item:pr-9 [@media(hover:none)]:pr-10", active && "bg-chat-selected");
4538
5106
  const press = () => {
4539
5107
  onPress?.();
4540
5108
  dismiss();
4541
5109
  };
4542
- const content = /* @__PURE__ */ jsxs(Fragment, { children: [
5110
+ const showIfCut = (e) => {
5111
+ const el = e.currentTarget;
5112
+ el.title = el.scrollWidth > el.clientWidth ? label : "";
5113
+ };
5114
+ const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [
4543
5115
  icon,
4544
- collapsed ? /* @__PURE__ */ jsx("span", {
5116
+ collapsed ? /* @__PURE__ */ jsxs("span", {
4545
5117
  className: "sr-only",
4546
- children: label
4547
- }) : /* @__PURE__ */ jsx("span", {
4548
- className: "min-w-0 flex-1 truncate",
4549
- children: label
5118
+ children: [label, (typeof badge === "string" || typeof badge === "number") && ` (${badge})`]
5119
+ }) : /* @__PURE__ */ jsxs("span", {
5120
+ className: "flex min-w-0 flex-1 flex-col",
5121
+ children: [/* @__PURE__ */ jsx("span", {
5122
+ className: "truncate",
5123
+ onPointerEnter: title ? void 0 : showIfCut,
5124
+ children: label
5125
+ }), description && /* @__PURE__ */ jsx("span", {
5126
+ className: "truncate text-(length:--chat-ui-font-size-sm) leading-4 text-chat-muted",
5127
+ children: description
5128
+ })]
4550
5129
  }),
4551
5130
  !collapsed && shortcut && /* @__PURE__ */ jsx("kbd", {
4552
5131
  className: "font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle opacity-0 group-data-hovered:opacity-100",
4553
5132
  children: shortcut
5133
+ }),
5134
+ !collapsed && badge != null && /* @__PURE__ */ jsx("span", {
5135
+ className: "shrink-0 text-(length:--chat-ui-font-size-sm) leading-4 text-chat-muted tabular-nums",
5136
+ children: badge
5137
+ }),
5138
+ collapsed && badge != null && /* @__PURE__ */ jsx("span", {
5139
+ "aria-hidden": true,
5140
+ className: "absolute top-1 right-1 size-2 rounded-full bg-chat-accent"
4554
5141
  })
4555
5142
  ] });
4556
5143
  const common = {
4557
5144
  className,
4558
5145
  onPress: press,
4559
- "aria-current": active ? "page" : void 0
5146
+ "aria-current": active ? "page" : void 0,
5147
+ ...title && { title }
4560
5148
  };
4561
5149
  const control = href ? /* @__PURE__ */ jsx(Link, {
4562
5150
  ...common,
@@ -4566,15 +5154,22 @@ function SidebarItem({ icon, label, onPress, href, active, shortcut }) {
4566
5154
  ...common,
4567
5155
  children: content
4568
5156
  });
4569
- return /* @__PURE__ */ jsx("li", { children: collapsed ? /* @__PURE__ */ jsx(Tooltip, {
4570
- content: label,
4571
- placement: "right",
4572
- children: control
4573
- }) : control });
5157
+ return /* @__PURE__ */ jsxs("li", {
5158
+ ...data,
5159
+ className: clsx("group/item relative list-none", extra),
5160
+ children: [collapsed ? /* @__PURE__ */ jsx(Tooltip, {
5161
+ content: label,
5162
+ placement: "right",
5163
+ children: control
5164
+ }) : control, hasTrailing && /* @__PURE__ */ jsx("div", {
5165
+ className: "absolute top-1/2 right-1 flex -translate-y-1/2 items-center opacity-0 group-hover/item:opacity-100 group-focus-within/item:opacity-100 [@media(hover:none)]:opacity-100",
5166
+ children: trailing
5167
+ })]
5168
+ });
4574
5169
  }
4575
5170
  const initials = (name) => name.split(/\s+/).slice(0, 2).map((w) => w[0]?.toUpperCase() ?? "").join("");
4576
5171
  /** The account row at the bottom: avatar, name, plan, and a menu. */
4577
- function SidebarUser({ name, detail, avatarUrl, menu }) {
5172
+ function SidebarUser({ name, detail, avatarUrl, menu, popoverProps }) {
4578
5173
  const icons = useIcons();
4579
5174
  const { collapsed } = useSidebar();
4580
5175
  const avatar = avatarUrl ? /* @__PURE__ */ jsx("img", {
@@ -4589,7 +5184,7 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
4589
5184
  const button = /* @__PURE__ */ jsxs(Button$1, {
4590
5185
  "aria-label": collapsed ? name : void 0,
4591
5186
  className: clsx("flex h-12 w-full cursor-pointer items-center gap-2.5 rounded-chat-md text-left outline-none", "data-hovered:bg-chat-hover data-pressed:bg-chat-selected data-focus-visible:ring-2 data-focus-visible:ring-chat-focus", collapsed ? "justify-center" : "px-1.5"),
4592
- children: [avatar, !collapsed && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
5187
+ children: [avatar, !collapsed && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("span", {
4593
5188
  className: "flex min-w-0 flex-1 flex-col",
4594
5189
  children: [/* @__PURE__ */ jsx("span", {
4595
5190
  className: "truncate text-(length:--chat-ui-font-size) leading-5 text-chat-fg",
@@ -4606,7 +5201,8 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
4606
5201
  if (!menu) return button;
4607
5202
  return /* @__PURE__ */ jsxs(MenuTrigger, { children: [button, /* @__PURE__ */ jsx(Popover, {
4608
5203
  placement: "top start",
5204
+ ...popoverProps,
4609
5205
  children: menu
4610
5206
  })] });
4611
5207
  }
4612
- 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, DictationButton, DictationControls, 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, VoiceMeter, WIDGET_MAX_HEIGHT, WidgetFrame, WidgetHostProvider, WidgetPartView, activityHeading, activityLabel, commandTool, defaultIcons, defaultLabels, defaultToolViews, encodeWav, formatDuration, formatRelativeTime, formatTokens, htmlComponent, intersectAllow, memoryTool, messageText, openInNewTab, readTreeInput, readVisualizeInput, sseTransport, uiMarkupToolView, uiTreeToolView, useAgentContext, useAgentRegistry, useAgentSnapshot, useAgentTool, useAttachments, useChat, useChatTool, useCountUp, useDictation, useIcons, useLabels, useMediaQuery, useSidebar, useSmoothText, useToolView, useToolViews, useUiAction, useUiMarkupLanguage, useUiState, useWidgetHost, userMessage, webFetchTool, webSearchTool, widgetCsp, widgetToolView };
5208
+ export { ActivityGroup, AgentProvider, AgentRegistry, AppShell, ApprovalCard, AssistantMessage, AttachButton, AttachmentCard, AttachmentRow, Badge, Button, Caret, ChatHeader, ChatList, ChatListRailButton, ChatNotice, ChatTheme, ChatTitleMenu, ChatTranscript, ChatView, CodeBlock, Composer, ContextMenu, ContextMeter, ContextPanel, CopyButton, CopyField, DRAWER_QUERY, DialogTrigger, DictationButton, DictationControls, Disclosure, Drawer, EffortButton, EffortSlider, EmptyState, IconButton, Markdown, MarkdownLinksProvider, Menu, MenuItem, MenuNote, MenuSearch, MenuSection, MenuSeparator, MenuTrigger, MessageList, Modal, ModelPicker, Panel, PastedTextDialog, Popover, PopoverTrigger, SegmentedControl, Sidebar, SidebarItem, SidebarList, SidebarToggle, SidebarUser, SubmenuTrigger, TokenCounter, ToolViewsProvider, Tooltip, UiComponentsProvider, UiMarkupBlock, UiTree, UserMessage, VoiceMeter, WIDGET_MAX_HEIGHT, WidgetFrame, WidgetHostProvider, WidgetPartView, activityHeading, activityLabel, commandTool, defaultIcons, defaultLabels, defaultToolViews, encodeWav, formatDuration, formatRelativeTime, formatTokens, htmlComponent, intersectAllow, memoryTool, messageText, openInNewTab, readTreeInput, readVisualizeInput, sseTransport, uiMarkupToolView, uiTreeToolView, useAgentContext, useAgentRegistry, useAgentSnapshot, useAgentTool, useAttachments, useChat, useChatTool, useCountUp, useDictation, useIcons, useLabels, useMediaQuery, useSidebar, useSmoothText, useToolView, useToolViews, useUiAction, useUiMarkupLanguage, useUiState, useWidgetHost, userMessage, webFetchTool, webSearchTool, widgetCsp, widgetToolView };