agentic-ui-kit 0.6.0 → 0.7.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,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";
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";
7
8
  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";
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 = {
98
+ ...props,
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, {
49
128
  ...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)
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
  }
@@ -170,6 +262,9 @@ const defaultLabels = {
170
262
  duration: (seconds) => seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`,
171
263
  ranCommandLabel: "Ran a command",
172
264
  ranCommands: (count) => plural(count, "ran a command", (n) => `ran ${n} commands`),
265
+ runCommandAsk: "Run this command?",
266
+ searchWebAsk: "Search the web?",
267
+ readPageAsk: "Read this page?",
173
268
  searchedWebLabel: "Searched the web",
174
269
  readPageLabel: "Read page",
175
270
  readPages: (count) => plural(count, "read a page", (n) => `read ${n} pages`),
@@ -181,7 +276,7 @@ const defaultLabels = {
181
276
  noResults: "No results",
182
277
  reasoningHidden: "The model's reasoning is not shown.",
183
278
  allowTool: (name) => `Allow ${name}?`,
184
- allowAction: (label) => /* @__PURE__ */ jsxs(Fragment, { children: [
279
+ allowAction: (label) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
185
280
  "Allow ",
186
281
  label,
187
282
  "?"
@@ -193,9 +288,14 @@ const defaultLabels = {
193
288
  openSidebar: "Open sidebar",
194
289
  expandSidebar: "Expand sidebar",
195
290
  collapseSidebar: "Collapse sidebar",
291
+ resizeSidebar: "Resize sidebar",
292
+ withShortcut: (label, keys) => `${label} (${keys})`,
293
+ ctrlKey: "Ctrl",
196
294
  context: "Context",
197
295
  contextUsed: "Context used",
198
296
  percentUsed: (percent) => `${percent}% used`,
297
+ contextSummary: (used, limit, percent) => `Context: ${used} / ${limit} (${percent}%)`,
298
+ contextLoading: "Context: loading",
199
299
  freeSpace: "Free space",
200
300
  tokenCount: (count) => `${count} tokens`,
201
301
  replyPlaceholder: "Reply…",
@@ -240,6 +340,7 @@ const defaultLabels = {
240
340
  justNow: "just now",
241
341
  copy: "Copy",
242
342
  copied: "Copied",
343
+ copyFieldLabel: "Link",
243
344
  retry: "Retry",
244
345
  edit: "Edit",
245
346
  readAloud: "Read aloud",
@@ -299,27 +400,40 @@ function CopyButton({ text, className }) {
299
400
  });
300
401
  }
301
402
  // src/primitives/segmented-control.tsx
403
+ const sizes = {
404
+ sm: {
405
+ button: "h-6",
406
+ iconOnly: "w-7"
407
+ },
408
+ md: {
409
+ button: "h-8",
410
+ iconOnly: "w-9"
411
+ }
412
+ };
302
413
  /**
303
414
  * One of a few options, side by side on a track, similar to claude.ai's Chat / Cowork switch. Themed with
304
415
  * `--chat-segment-track` (default: a shade darker than the surface it sits on) and `--chat-segment-selected`
305
416
  * (default `--chat-raised`).
306
417
  */
307
- function SegmentedControl({ label, options, value, onChange, className }) {
418
+ function SegmentedControl({ label, options, value, onChange, clearable, isDisabled, size = "sm", className }) {
308
419
  return /* @__PURE__ */ jsx(ToggleButtonGroup, {
309
420
  "aria-label": label,
310
421
  selectionMode: "single",
311
- disallowEmptySelection: true,
312
- selectedKeys: [value],
422
+ disallowEmptySelection: !clearable,
423
+ ...isDisabled && { isDisabled },
424
+ selectedKeys: value === null ? [] : [value],
313
425
  onSelectionChange: (keys) => {
314
426
  const next = [...keys][0];
315
427
  if (next !== void 0) onChange(next);
428
+ else if (clearable) onChange(null);
316
429
  },
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),
430
+ 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
431
  children: options.map((o) => {
319
432
  const button = /* @__PURE__ */ jsxs(ToggleButton, {
320
433
  id: o.id,
434
+ ...o.isDisabled && { isDisabled: true },
321
435
  ...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)]"),
436
+ 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
437
  children: [o.icon, !o.iconOnly && o.label]
324
438
  }, o.id);
325
439
  return o.iconOnly ? /* @__PURE__ */ jsx(Tooltip, {
@@ -340,7 +454,7 @@ function Disclosure({ summary, children, className, summaryClassName, panelClass
340
454
  className: "m-0 font-normal",
341
455
  children: /* @__PURE__ */ jsxs(Button$1, {
342
456
  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),
457
+ 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
458
  children: [
345
459
  indicator === "chevron" && /* @__PURE__ */ jsx(IconSlot, {
346
460
  className: "transition-transform group-aria-expanded/trigger:rotate-90 [&_svg]:size-3.5",
@@ -367,6 +481,11 @@ function Disclosure({ summary, children, className, summaryClassName, panelClass
367
481
  });
368
482
  }
369
483
  // src/primitives/menu.tsx
484
+ /**
485
+ * Wraps a trigger button and a kit `Popover` (or `Modal`) so that pressing the button toggles it, without
486
+ * importing react-aria. The same component as `DialogTrigger`; use whichever name reads better.
487
+ */
488
+ const PopoverTrigger = DialogTrigger$1;
370
489
  /** A search box above a menu that filters its items, like "Search or create a project". */
371
490
  function MenuSearch({ placeholder, children, value, onChange }) {
372
491
  const { contains } = useFilter({ sensitivity: "base" });
@@ -385,41 +504,71 @@ function MenuSearch({ placeholder, children, value, onChange }) {
385
504
  }), children]
386
505
  });
387
506
  }
388
- /** Floating panel for menus and pickers: surface color, rounded, soft shadow, fades in. */
389
- function Popover({ className, offset = 6, ...props }) {
507
+ /** Viewport narrower than `max` px. False on the server and where `matchMedia` is missing. */
508
+ function useNarrower(max) {
509
+ const query = max === false ? null : `(max-width: ${max - .02}px)`;
510
+ return useSyncExternalStore((notify) => {
511
+ if (!query || typeof window === "undefined" || !window.matchMedia) return () => {};
512
+ const list = window.matchMedia(query);
513
+ list.addEventListener("change", notify);
514
+ return () => list.removeEventListener("change", notify);
515
+ }, () => query && typeof window !== "undefined" && window.matchMedia ? window.matchMedia(query).matches : false, () => false);
516
+ }
517
+ /** Floating panel for menus and pickers: surface color, rounded, soft shadow, fades in. On phones it's a bottom sheet. */
518
+ function Popover({ className, offset = 6, sheet = 768, width = "auto", fullWidthOnPhones, children, ...props }) {
519
+ if (useNarrower(sheet)) {
520
+ const { isOpen, onOpenChange } = props;
521
+ return /* @__PURE__ */ jsx(ModalOverlay, {
522
+ ...isOpen !== void 0 && { isOpen },
523
+ ...onOpenChange && { onOpenChange },
524
+ isDismissable: true,
525
+ className: "fixed inset-0 z-50 flex items-end bg-(--chat-backdrop) data-entering:animate-chat-fade-in data-exiting:animate-chat-fade-out",
526
+ children: /* @__PURE__ */ jsx(Modal$1, {
527
+ 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),
528
+ children: typeof children === "function" ? children({
529
+ placement: "bottom",
530
+ trigger: null,
531
+ isEntering: false,
532
+ isExiting: false,
533
+ defaultChildren: void 0
534
+ }) : children
535
+ })
536
+ });
537
+ }
390
538
  return /* @__PURE__ */ jsx(Popover$1, {
391
539
  offset,
392
540
  ...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)
541
+ 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),
542
+ children
394
543
  });
395
544
  }
396
545
  function Menu({ className, ...props }) {
397
546
  return /* @__PURE__ */ jsx(Menu$1, {
398
547
  ...props,
399
- className: clsx("max-h-[inherit] min-w-52 overflow-auto p-1.5 outline-none", className)
548
+ className: cx("max-h-[inherit] min-w-52 overflow-auto p-1.5 outline-none", className)
400
549
  });
401
550
  }
402
- function MenuItem({ children, icon, description, descriptionPlacement = "below", detail, shortcut, tone = "default", className, ...props }) {
551
+ function MenuItem({ children, icon, description, descriptionPlacement = "below", detail, shortcut, tone = "default", indicator = "check", className, ...props }) {
403
552
  const icons = useIcons();
404
553
  const textValue = props.textValue ?? (typeof children === "string" ? children : void 0);
405
554
  return /* @__PURE__ */ jsx(MenuItem$1, {
406
555
  ...props,
407
556
  ...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: [
557
+ 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),
558
+ children: ({ isSelected, hasSubmenu, selectionMode }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
410
559
  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"),
560
+ className: cx("flex size-4 shrink-0 items-center [&_svg]:size-4", tone === "danger" ? "text-chat-danger" : "text-chat-fg"),
412
561
  children: icon
413
562
  }),
414
563
  /* @__PURE__ */ jsxs("span", {
415
- className: clsx("flex min-w-0 flex-1", descriptionPlacement === "beside" ? "items-baseline gap-2" : "flex-col"),
564
+ className: cx("flex min-w-0 flex-1", descriptionPlacement === "beside" ? "items-baseline gap-2" : "flex-col"),
416
565
  children: [/* @__PURE__ */ jsx(Text, {
417
566
  slot: "label",
418
- className: clsx("truncate", descriptionPlacement === "beside" && "shrink-0"),
567
+ className: cx("truncate", descriptionPlacement === "beside" && "shrink-0"),
419
568
  children
420
569
  }), description && /* @__PURE__ */ jsx(Text, {
421
570
  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"),
571
+ 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
572
  children: description
424
573
  })]
425
574
  }),
@@ -431,7 +580,12 @@ function MenuItem({ children, icon, description, descriptionPlacement = "below",
431
580
  className: "shrink-0 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle",
432
581
  children: shortcut
433
582
  }),
434
- selectionMode !== "none" && isSelected && /* @__PURE__ */ jsx(IconSlot, {
583
+ selectionMode !== "none" && indicator === "switch" && /* @__PURE__ */ jsx("span", {
584
+ "aria-hidden": true,
585
+ "data-selected": isSelected || void 0,
586
+ 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"
587
+ }),
588
+ selectionMode !== "none" && indicator === "check" && isSelected && /* @__PURE__ */ jsx(IconSlot, {
435
589
  className: "text-chat-check [&_svg]:size-4",
436
590
  children: icons.check
437
591
  }),
@@ -458,7 +612,7 @@ function MenuSeparator() {
458
612
  /** Explanatory text inside a menu, above or below the items. Not focusable. */
459
613
  function MenuNote({ children, className }) {
460
614
  return /* @__PURE__ */ jsx("div", {
461
- className: clsx("px-2 py-1.5 text-(length:--chat-ui-font-size) leading-snug text-chat-muted", className),
615
+ className: cx("px-2 py-1.5 text-(length:--chat-ui-font-size) leading-snug text-chat-muted", className),
462
616
  children
463
617
  });
464
618
  }
@@ -492,7 +646,7 @@ function ContextMenu({ menu, children, className }) {
492
646
  ref: root,
493
647
  onContextMenu,
494
648
  onKeyDown,
495
- className: clsx("relative", className),
649
+ className: cx("relative", className),
496
650
  children: [children, /* @__PURE__ */ jsxs(MenuTrigger$1, {
497
651
  isOpen: at !== void 0,
498
652
  onOpenChange: (open) => !open && setAt(void 0),
@@ -513,15 +667,23 @@ function ContextMenu({ menu, children, className }) {
513
667
  });
514
668
  }
515
669
  // src/primitives/overlay.tsx
670
+ const modalSizes = {
671
+ sm: "max-w-sm",
672
+ md: "max-w-lg",
673
+ lg: "max-w-2xl",
674
+ xl: "max-w-5xl",
675
+ /** Fills the page inside the overlay's padding. */
676
+ full: "h-full max-h-none max-w-none"
677
+ };
516
678
  /** 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 }) {
679
+ function Modal({ isOpen, onOpenChange, title, size = "lg", placement = "center", children, className, ...props }) {
518
680
  return /* @__PURE__ */ jsx(ModalOverlay, {
519
681
  isOpen,
520
682
  onOpenChange,
521
683
  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",
684
+ 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
685
  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),
686
+ 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
687
  children: /* @__PURE__ */ jsxs(Dialog, {
526
688
  ...props,
527
689
  className: "flex min-h-0 flex-1 flex-col outline-none",
@@ -542,7 +704,7 @@ function Drawer({ isOpen, onOpenChange, side = "left", children, className, ...p
542
704
  isDismissable: true,
543
705
  className: "fixed inset-0 z-40 bg-(--chat-backdrop) data-entering:animate-chat-fade-in data-exiting:animate-chat-fade-out",
544
706
  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),
707
+ 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
708
  children: /* @__PURE__ */ jsx(Dialog, {
547
709
  ...props,
548
710
  className: "h-full outline-none",
@@ -559,7 +721,7 @@ function Drawer({ isOpen, onOpenChange, side = "left", children, className, ...p
559
721
  function Panel({ title, actions, footer, children, variant = "card", className, ...props }) {
560
722
  return /* @__PURE__ */ jsxs("section", {
561
723
  "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),
724
+ 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
725
  children: [
564
726
  (title || actions) && /* @__PURE__ */ jsxs("header", {
565
727
  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",
@@ -726,7 +888,7 @@ function CodeBlock({ code, language, label = language, variant = "plain", copy =
726
888
  }), copy && /* @__PURE__ */ jsx(CopyButton, { text: code })]
727
889
  }), /* @__PURE__ */ jsx("pre", {
728
890
  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", {
891
+ 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
892
  className: "chat-code-token",
731
893
  style: token.htmlStyle,
732
894
  children: token.content
@@ -881,7 +1043,7 @@ const DECLINED = "Declined by the user";
881
1043
  * retries. With an `AgentProvider` it also runs the agent loop: client tools run in the page, approvals
882
1044
  * wait for the user, then the reply continues; every finished tool call reaches its tool's `onResult`.
883
1045
  */
884
- function useChat({ transport, conversationId, initialMessages = [], onTitle, agent, maxSteps = 12 }) {
1046
+ function useChat({ transport, conversationId, initialMessages = [], onTitle, agent, maxSteps = 12, resume = false }) {
885
1047
  const contextAgent = useAgentRegistry();
886
1048
  const registry = agent === void 0 ? contextAgent : agent;
887
1049
  const [messages, setMessages] = useState(initialMessages);
@@ -949,7 +1111,7 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
949
1111
  if (ended) commit(ended);
950
1112
  }, [commit]);
951
1113
  const advanceRef = useRef(() => {});
952
- const start = useCallback((history, options, continuation) => {
1114
+ const start = useCallback((history, options, continuation, joining) => {
953
1115
  const message = history.at(-1);
954
1116
  const controller = new AbortController();
955
1117
  const current = {
@@ -965,7 +1127,7 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
965
1127
  try {
966
1128
  const { transport, conversationId, registry } = latest.current;
967
1129
  const tools = registry?.specs();
968
- for await (const event of transport.send({
1130
+ const events = joining && conversationId !== void 0 && transport.resume ? transport.resume(conversationId, controller.signal) : transport.send({
969
1131
  message,
970
1132
  history,
971
1133
  signal: controller.signal,
@@ -975,7 +1137,8 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
975
1137
  ...conversationId !== void 0 && { conversationId },
976
1138
  ...options.model !== void 0 && { model: options.model },
977
1139
  ...options.effort !== void 0 && { effort: options.effort }
978
- })) {
1140
+ });
1141
+ for await (const event of events) {
979
1142
  if (controller.signal.aborted) break;
980
1143
  if (event.type === "title") {
981
1144
  latest.current.onTitle?.(event.title);
@@ -1147,7 +1310,30 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
1147
1310
  };
1148
1311
  start(messages.slice(0, index), options);
1149
1312
  }, [start]);
1150
- useEffect(() => () => run.current?.controller.abort(), []);
1313
+ const joined = useRef(false);
1314
+ useEffect(() => {
1315
+ if (!resume || joined.current || run.current) return;
1316
+ const { transport, conversationId, messages } = latest.current;
1317
+ if (!transport.resume || conversationId === void 0) return;
1318
+ joined.current = true;
1319
+ const last = messages.at(-1);
1320
+ const history = last?.role === "assistant" && last.status === "streaming" ? messages.slice(0, -1) : messages;
1321
+ loop.current = {
1322
+ options: {},
1323
+ step: 0
1324
+ };
1325
+ start(history, {}, void 0, true);
1326
+ }, [resume, start]);
1327
+ const mounted = useRef(false);
1328
+ useEffect(() => {
1329
+ mounted.current = true;
1330
+ return () => {
1331
+ mounted.current = false;
1332
+ queueMicrotask(() => {
1333
+ if (!mounted.current) run.current?.controller.abort();
1334
+ });
1335
+ };
1336
+ }, []);
1151
1337
  return {
1152
1338
  messages,
1153
1339
  busy,
@@ -1160,7 +1346,7 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
1160
1346
  }
1161
1347
  // src/hooks/sse-transport.ts
1162
1348
  /** A `Transport` for backends that stream `StreamEvent`s over server-sent events (see `formatEvent`). */
1163
- function sseTransport({ url, headers, stopUrl, fetch: fetchImpl = fetch }) {
1349
+ function sseTransport({ url, headers, stopUrl, resumeUrl, fetch: fetchImpl = fetch }) {
1164
1350
  const headersFor = async () => ({
1165
1351
  "content-type": "application/json",
1166
1352
  ...Object.fromEntries(new Headers(typeof headers === "function" ? await headers() : headers))
@@ -1176,6 +1362,20 @@ function sseTransport({ url, headers, stopUrl, fetch: fetchImpl = fetch }) {
1176
1362
  if (!response.ok || !response.body) throw new Error(`${url} answered ${response.status} ${response.statusText}`.trim());
1177
1363
  yield* readEventStream(response.body);
1178
1364
  },
1365
+ ...resumeUrl && { async *resume(conversationId, signal) {
1366
+ const target = typeof resumeUrl === "function" ? resumeUrl(conversationId) : `${resumeUrl}${resumeUrl.includes("?") ? "&" : "?"}conversationId=${encodeURIComponent(conversationId)}`;
1367
+ const { "content-type": _, ...headers } = await headersFor();
1368
+ const response = await fetchImpl(target, {
1369
+ method: "GET",
1370
+ headers: {
1371
+ ...headers,
1372
+ accept: "text/event-stream"
1373
+ },
1374
+ signal
1375
+ });
1376
+ if (!response.ok || !response.body) throw new Error(`${target} answered ${response.status} ${response.statusText}`.trim());
1377
+ yield* readEventStream(response.body);
1378
+ } },
1179
1379
  ...stopUrl && { async stop(runId) {
1180
1380
  await fetchImpl(stopUrl, {
1181
1381
  method: "POST",
@@ -1280,6 +1480,30 @@ const SidebarContext = createContext({
1280
1480
  dismiss: () => {}
1281
1481
  });
1282
1482
  const useSidebar = () => useContext(SidebarContext);
1483
+ /** State that the parent may own: `value` makes it controlled, otherwise `initial` seeds it. */
1484
+ function useControllable(value, initial, onChange) {
1485
+ const [inner, setInner] = useState(initial);
1486
+ const current = value ?? inner;
1487
+ const latest = useRef({
1488
+ current,
1489
+ value,
1490
+ onChange
1491
+ });
1492
+ latest.current = {
1493
+ current,
1494
+ value,
1495
+ onChange
1496
+ };
1497
+ return [current, useCallback((next) => {
1498
+ const { current, value, onChange } = latest.current;
1499
+ const resolved = typeof next === "function" ? next(current) : next;
1500
+ if (resolved === current) return;
1501
+ if (value === void 0) setInner(resolved);
1502
+ onChange?.(resolved);
1503
+ }, [])];
1504
+ }
1505
+ /** True on Apple platforms, where the shortcut modifier is ⌘ instead of Ctrl. */
1506
+ const isMacPlatform = () => typeof navigator !== "undefined" && /mac|iphone|ipad|ipod/i.test(navigator.platform || navigator.userAgent);
1283
1507
  // src/components/replacements.tsx
1284
1508
  /**
1285
1509
  * In-place updates of drawn visuals: a tool call whose input has `replaces: <id>` takes the place of that
@@ -2193,7 +2417,7 @@ function Code({ node, className, children }) {
2193
2417
  });
2194
2418
  }
2195
2419
  const components = {
2196
- pre: ({ children }) => /* @__PURE__ */ jsx(Fragment, { children }),
2420
+ pre: ({ children }) => /* @__PURE__ */ jsx(Fragment$1, { children }),
2197
2421
  code: ({ node, className, children }) => /* @__PURE__ */ jsx(Code, {
2198
2422
  node,
2199
2423
  className,
@@ -2201,12 +2425,39 @@ const components = {
2201
2425
  })
2202
2426
  };
2203
2427
  const LinksContext = createContext(void 0);
2204
- function MarkdownLinksProvider({ renderLink, children }) {
2428
+ function MarkdownLinksProvider({ renderLink, renderLinkList, children }) {
2429
+ const value = useMemo(() => ({
2430
+ renderLink,
2431
+ renderLinkList
2432
+ }), [renderLink, renderLinkList]);
2205
2433
  return /* @__PURE__ */ jsx(LinksContext.Provider, {
2206
- value: renderLink,
2434
+ value,
2207
2435
  children
2208
2436
  });
2209
2437
  }
2438
+ /** The rendered link an item consists of: an `a` alone, or alone in its paragraph (a loose list). */
2439
+ function soleLink(children) {
2440
+ const nodes = Children.toArray(children).filter((c) => !(typeof c === "string" && !c.trim()));
2441
+ if (nodes.length !== 1 || !isValidElement(nodes[0])) return void 0;
2442
+ const props = nodes[0].props;
2443
+ if (typeof props.href === "string") return {
2444
+ href: props.href,
2445
+ children: props.children
2446
+ };
2447
+ return props.node?.tagName === "p" ? soleLink(props.children) : void 0;
2448
+ }
2449
+ function listOf(ordered, props, renderLinkList) {
2450
+ const Default = ordered ? defaultComponents.ol : defaultComponents.ul;
2451
+ const { children } = props;
2452
+ const links = Children.toArray(children).filter((c) => isValidElement(c)).map((item) => soleLink(item.props.children));
2453
+ const list = /* @__PURE__ */ jsx(Default, { ...props });
2454
+ if (links.length === 0 || links.some((l) => !l)) return list;
2455
+ return renderLinkList({
2456
+ ordered,
2457
+ links,
2458
+ children: list
2459
+ }) ?? list;
2460
+ }
2210
2461
  /** Message markdown: GFM, KaTeX math ($…$ and $$…$$), Shiki code blocks, smooth fade-in while streaming. */
2211
2462
  function Markdown({ children, streaming = false, className, partId }) {
2212
2463
  const text = useSmoothText(children, streaming);
@@ -2237,22 +2488,26 @@ function BlockScope({ id, streaming, children }) {
2237
2488
  });
2238
2489
  }
2239
2490
  const MarkdownBody = memo(function MarkdownBody({ text, animating, fade }) {
2240
- const renderLink = useContext(LinksContext);
2241
- const withLinks = useMemo(() => renderLink ? {
2491
+ const { renderLink, renderLinkList } = useContext(LinksContext) ?? {};
2492
+ const withLinks = useMemo(() => ({
2242
2493
  ...components,
2243
- a: ({ href, children }) => {
2494
+ ...renderLink && { a: ({ href, children }) => {
2244
2495
  const custom = href ? renderLink({
2245
2496
  href,
2246
2497
  children
2247
2498
  }) : void 0;
2248
- return custom !== void 0 ? /* @__PURE__ */ jsx(Fragment, { children: custom }) : /* @__PURE__ */ jsx("a", {
2499
+ return custom !== void 0 ? /* @__PURE__ */ jsx(Fragment$1, { children: custom }) : /* @__PURE__ */ jsx("a", {
2249
2500
  href,
2250
2501
  target: "_blank",
2251
2502
  rel: "noopener noreferrer",
2252
2503
  children
2253
2504
  });
2505
+ } },
2506
+ ...renderLinkList && {
2507
+ ul: (props) => listOf(false, props, renderLinkList),
2508
+ ol: (props) => listOf(true, props, renderLinkList)
2254
2509
  }
2255
- } : components, [renderLink]);
2510
+ }), [renderLink, renderLinkList]);
2256
2511
  return /* @__PURE__ */ jsx(Streamdown, {
2257
2512
  prefix: PREFIX,
2258
2513
  mode: animating || fade ? "streaming" : "static",
@@ -2312,7 +2567,7 @@ function activityLabel(part, labels, view) {
2312
2567
  */
2313
2568
  function activityHeading(parts, views, labels) {
2314
2569
  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] });
2570
+ if (waiting) return /* @__PURE__ */ jsxs(Fragment$1, { children: [activityLabel(waiting, labels, views[waiting.name]), labels.waitingForYou] });
2316
2571
  if (parts.some(isWorking)) {
2317
2572
  const last = parts.at(-1);
2318
2573
  return activityLabel(last, labels, last.type === "tool_call" ? views[last.name] : void 0);
@@ -2331,6 +2586,7 @@ function stringOf(value) {
2331
2586
  }
2332
2587
  /** A shell command. Input `{ command, description? }`; result a string or `{ stdout, stderr, exitCode }`. */
2333
2588
  const commandTool = {
2589
+ ask: (_, labels) => labels.runCommandAsk,
2334
2590
  label: (_, labels) => labels.ranCommandLabel,
2335
2591
  hint: (part) => stringOf(recordOf(part.input).description),
2336
2592
  summary: (n, labels) => labels.ranCommands(n),
@@ -2363,6 +2619,7 @@ const commandTool = {
2363
2619
  * part's display `data`, else its `result`.
2364
2620
  */
2365
2621
  const webSearchTool = {
2622
+ ask: (_, labels) => labels.searchWebAsk,
2366
2623
  label: (_, labels) => labels.searchedWebLabel,
2367
2624
  subtitle: (part) => stringOf(recordOf(part.input).query) ?? part.subtitle,
2368
2625
  summary: (n, labels) => labels.searchedWeb(n),
@@ -2410,6 +2667,7 @@ const webSearchTool = {
2410
2667
  };
2411
2668
  /** Reads a web page. Input `{ url }`; the row shows the page's host, the detail links to it. */
2412
2669
  const webFetchTool = {
2670
+ ask: (_, labels) => labels.readPageAsk,
2413
2671
  label: (_, labels) => labels.readPageLabel,
2414
2672
  subtitle: (part) => {
2415
2673
  const url = stringOf(recordOf(part.input).url);
@@ -2550,7 +2808,7 @@ function ActivityGroup({ parts, defaultExpanded = false, className }) {
2550
2808
  }) : /* @__PURE__ */ jsx(Disclosure, {
2551
2809
  isExpanded: expanded,
2552
2810
  onExpandedChange: setExpanded,
2553
- summary: /* @__PURE__ */ jsxs(Fragment, { children: [
2811
+ summary: /* @__PURE__ */ jsxs(Fragment$1, { children: [
2554
2812
  /* @__PURE__ */ jsx(Lead, { live }),
2555
2813
  /* @__PURE__ */ jsx(Live, {
2556
2814
  live,
@@ -2584,7 +2842,7 @@ const SHIMMER_MS = 2e3;
2584
2842
  * shared clock, so every live label sweeps in step and a remount does not restart it.
2585
2843
  */
2586
2844
  function Live({ live, children }) {
2587
- return live ? /* @__PURE__ */ jsx(Shimmer, { children }) : /* @__PURE__ */ jsx(Fragment, { children });
2845
+ return live ? /* @__PURE__ */ jsx(Shimmer, { children }) : /* @__PURE__ */ jsx(Fragment$1, { children });
2588
2846
  }
2589
2847
  function Shimmer({ children }) {
2590
2848
  const [delay] = useState(() => `-${Math.round(performance.now() % SHIMMER_MS)}ms`);
@@ -2602,7 +2860,7 @@ function SingleStep({ part, live, after, expanded, onExpandedChange }) {
2602
2860
  return /* @__PURE__ */ jsx(Disclosure, {
2603
2861
  isExpanded: expanded,
2604
2862
  onExpandedChange,
2605
- summary: /* @__PURE__ */ jsxs(Fragment, { children: [
2863
+ summary: /* @__PURE__ */ jsxs(Fragment$1, { children: [
2606
2864
  /* @__PURE__ */ jsx(Lead, { live }),
2607
2865
  /* @__PURE__ */ jsx(Live, {
2608
2866
  live,
@@ -2773,11 +3031,19 @@ function useAttachments({ upload } = {}) {
2773
3031
  toParts
2774
3032
  };
2775
3033
  }
3034
+ const IMAGE_EXTENSION = /\.(png|jpe?g|gif|webp|avif|svg|bmp|ico)$/i;
3035
+ /** Whether the card draws a thumbnail: by mime when given, else by `kind`, else by the file extension of the url or name. */
3036
+ function isImage({ mime, kind, url, name }) {
3037
+ if (mime) return mime.startsWith("image/");
3038
+ if (kind) return kind === "image";
3039
+ const path = url && !url.startsWith("data:") ? url.split(/[?#]/)[0] : "";
3040
+ return url?.startsWith("data:image/") === true || IMAGE_EXTENSION.test(path) || IMAGE_EXTENSION.test(name);
3041
+ }
2776
3042
  const extension = (name) => /\.([a-z0-9]{1,5})$/i.exec(name)?.[1]?.toUpperCase();
2777
3043
  /** A square card: pasted text preview with a PASTED badge, an image thumbnail, or a file with its type. */
2778
3044
  function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", className }) {
2779
3045
  const icons = useIcons();
2780
- const image = a.mime.startsWith("image/") && a.url;
3046
+ const image = a.url && isImage(a);
2781
3047
  const pasted = a.text !== void 0;
2782
3048
  const labels = useLabels();
2783
3049
  const uploading = a.status === "uploading";
@@ -2842,7 +3108,7 @@ function AttachmentRow({ attachments, onRemove, onEditText }) {
2842
3108
  const [editing, setEditing] = useState();
2843
3109
  const labels = useLabels();
2844
3110
  if (attachments.length === 0) return null;
2845
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("ul", {
3111
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("ul", {
2846
3112
  "aria-label": labels.attachments,
2847
3113
  className: "flex gap-2.5 overflow-x-auto px-0.5 pt-2.5 pb-1 [scrollbar-width:thin]",
2848
3114
  children: attachments.map((a) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(AttachmentCard, {
@@ -3012,7 +3278,7 @@ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPar
3012
3278
  if (block.kind === "activity") {
3013
3279
  const cards = block.parts.filter((p) => p.type === "tool_call" && views[p.name]?.Card);
3014
3280
  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) => {
3281
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ActivityGroup, { parts: block.parts }), cards.map((p) => {
3016
3282
  if (p.type !== "tool_call") return null;
3017
3283
  const Card = views[p.name].Card;
3018
3284
  return /* @__PURE__ */ jsx(Card, { part: p }, p.id);
@@ -3054,7 +3320,7 @@ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPar
3054
3320
  onPress: onReadAloud,
3055
3321
  children: icons.speak
3056
3322
  }),
3057
- onFeedback && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(IconButton, {
3323
+ onFeedback && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(IconButton, {
3058
3324
  label: labels.goodResponse,
3059
3325
  size: "sm",
3060
3326
  onPress: () => onFeedback("good"),
@@ -3108,7 +3374,7 @@ function ApprovalCard({ approval, onRespond, className }) {
3108
3374
  children: icons.approval
3109
3375
  }), /* @__PURE__ */ jsx("span", {
3110
3376
  className: "min-w-0 flex-1 font-medium",
3111
- children: view.ask?.(part) ?? labels.allowAction(activityLabel(part, labels, view))
3377
+ children: view.ask?.(part, labels) ?? labels.allowAction(view.askLabel?.(part, labels) ?? activityLabel(part, labels, view))
3112
3378
  })]
3113
3379
  }),
3114
3380
  /* @__PURE__ */ jsx("div", {
@@ -3139,6 +3405,16 @@ function ApprovalCard({ approval, onRespond, className }) {
3139
3405
  // src/components/context-panel.tsx
3140
3406
  const FREE_COLOR = "var(--chat-raised)";
3141
3407
  const DEFAULT_COLOR = "var(--chat-subtle)";
3408
+ /** Tokens used (reserved and deferred categories excluded) and tokens still free. */
3409
+ function contextTotals(usage) {
3410
+ const counted = usage.categories.filter((c) => !c.deferred);
3411
+ const used = counted.reduce((sum, c) => sum + (c.reserved ? 0 : c.tokens), 0);
3412
+ const reserved = counted.reduce((sum, c) => sum + (c.reserved ? c.tokens : 0), 0);
3413
+ return {
3414
+ used,
3415
+ free: Math.max(usage.limit - used - reserved, 0)
3416
+ };
3417
+ }
3142
3418
  /**
3143
3419
  * What fills the context window: a usage line, a segmented bar, one row per category with its share, and
3144
3420
  * expandable lists (each MCP tool, each memory file). Deferred categories are listed but not counted.
@@ -3147,9 +3423,7 @@ function ContextPanel({ usage, title, defaultExpanded = true, className }) {
3147
3423
  const labels = useLabels();
3148
3424
  const counted = usage.categories.filter((c) => !c.deferred);
3149
3425
  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);
3426
+ const { used, free } = contextTotals(usage);
3153
3427
  const share = (tokens) => formatShare(tokens / usage.limit);
3154
3428
  return /* @__PURE__ */ jsxs(Disclosure, {
3155
3429
  summary: title ?? labels.context,
@@ -3274,6 +3548,121 @@ function GroupList({ group }) {
3274
3548
  function formatShare(fraction) {
3275
3549
  return `${(fraction * 100).toFixed(1)}%`;
3276
3550
  }
3551
+ // src/components/context-meter.tsx
3552
+ const SIZE = 16;
3553
+ const STROKE = 2;
3554
+ const RADIUS = 7;
3555
+ const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
3556
+ /**
3557
+ * A small progress ring for the composer toolbar, filled to the share of the context window in use (accent,
3558
+ * then warning, then danger). Hover for the numbers; click or Enter opens the ContextPanel above it.
3559
+ */
3560
+ function ContextMeter({ usage, tooltip, tooltipNote, isLoading = false, warnAt = .75, dangerAt = .9, placement = "top end", className }) {
3561
+ const labels = useLabels();
3562
+ const { used } = contextTotals(usage);
3563
+ const fraction = usage.limit > 0 ? Math.min(used / usage.limit, 1) : 0;
3564
+ const summary = isLoading ? labels.contextLoading : labels.contextSummary(formatTokens(used), formatTokens(usage.limit), Math.round(fraction * 100));
3565
+ const color = fraction >= dangerAt ? "text-chat-danger" : fraction >= warnAt ? "text-chat-warning" : "text-chat-accent";
3566
+ const button = /* @__PURE__ */ jsx(Button, {
3567
+ variant: "ghost",
3568
+ size: "sm",
3569
+ "aria-label": summary,
3570
+ isDisabled: isLoading,
3571
+ className: clsx("size-7 justify-center px-0 text-chat-muted", isLoading && "data-disabled:opacity-100", className),
3572
+ children: /* @__PURE__ */ jsxs("svg", {
3573
+ "aria-hidden": true,
3574
+ viewBox: `0 0 ${SIZE} ${SIZE}`,
3575
+ className: clsx("size-4", isLoading && "motion-safe:animate-spin"),
3576
+ fill: "none",
3577
+ strokeWidth: STROKE,
3578
+ children: [/* @__PURE__ */ jsx("circle", {
3579
+ cx: SIZE / 2,
3580
+ cy: SIZE / 2,
3581
+ r: RADIUS,
3582
+ stroke: "var(--chat-raised)"
3583
+ }), /* @__PURE__ */ jsx("circle", {
3584
+ cx: SIZE / 2,
3585
+ cy: SIZE / 2,
3586
+ r: RADIUS,
3587
+ stroke: "currentColor",
3588
+ strokeLinecap: "round",
3589
+ strokeDasharray: CIRCUMFERENCE,
3590
+ strokeDashoffset: CIRCUMFERENCE * (1 - (isLoading ? .25 : fraction)),
3591
+ transform: `rotate(-90 ${SIZE / 2} ${SIZE / 2})`,
3592
+ className: clsx(color, "transition-[stroke-dashoffset]")
3593
+ })]
3594
+ })
3595
+ });
3596
+ return /* @__PURE__ */ jsxs(DialogTrigger$1, { children: [/* @__PURE__ */ jsx(Tooltip, {
3597
+ content: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", { children: tooltip ?? summary }), tooltipNote && /* @__PURE__ */ jsx("div", {
3598
+ className: "text-chat-muted",
3599
+ children: tooltipNote
3600
+ })] }),
3601
+ children: button
3602
+ }), /* @__PURE__ */ jsx(Popover, {
3603
+ placement,
3604
+ className: "w-80 overflow-auto p-3",
3605
+ children: /* @__PURE__ */ jsx(Dialog, {
3606
+ "aria-label": labels.context,
3607
+ className: "outline-none",
3608
+ children: /* @__PURE__ */ jsx(ContextPanel, { usage })
3609
+ })
3610
+ })] });
3611
+ }
3612
+ // src/components/copy-field.tsx
3613
+ /** How long "Copied" stays before the button reads "Copy" again. */
3614
+ const COPIED_MS = 2e3;
3615
+ /**
3616
+ * A read-only, selectable field with a Copy button, for share links. The button says "Copied" for about two
3617
+ * seconds (and announces it to screen readers). Where the clipboard is unavailable or refuses, it selects the
3618
+ * text instead so the reader can copy it by hand.
3619
+ */
3620
+ function CopyField({ value, "aria-label": ariaLabel, onCopy, className }) {
3621
+ const icons = useIcons();
3622
+ const labels = useLabels();
3623
+ const inputRef = useRef(null);
3624
+ const timer = useRef(void 0);
3625
+ const [copied, setCopied] = useState(false);
3626
+ useEffect(() => () => clearTimeout(timer.current), []);
3627
+ const copy = async () => {
3628
+ try {
3629
+ if (!navigator.clipboard) throw new Error("no clipboard");
3630
+ await navigator.clipboard.writeText(value);
3631
+ } catch {
3632
+ inputRef.current?.focus();
3633
+ inputRef.current?.select();
3634
+ return;
3635
+ }
3636
+ setCopied(true);
3637
+ onCopy?.();
3638
+ clearTimeout(timer.current);
3639
+ timer.current = setTimeout(() => setCopied(false), COPIED_MS);
3640
+ };
3641
+ return /* @__PURE__ */ jsxs("div", {
3642
+ className: clsx("flex items-center gap-2 font-chat-sans", className),
3643
+ children: [
3644
+ /* @__PURE__ */ jsx("input", {
3645
+ ref: inputRef,
3646
+ readOnly: true,
3647
+ value,
3648
+ "aria-label": ariaLabel ?? labels.copyFieldLabel,
3649
+ onFocus: (e) => e.currentTarget.select(),
3650
+ 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"
3651
+ }),
3652
+ /* @__PURE__ */ jsxs(Button, {
3653
+ variant: "subtle",
3654
+ size: "lg",
3655
+ onPress: () => void copy(),
3656
+ children: [/* @__PURE__ */ jsx(IconSlot, { children: copied ? icons.copied : icons.copy }), copied ? labels.copied : labels.copy]
3657
+ }),
3658
+ /* @__PURE__ */ jsx("span", {
3659
+ role: "status",
3660
+ className: "sr-only",
3661
+ children: copied ? labels.copied : ""
3662
+ })
3663
+ ]
3664
+ });
3665
+ }
3277
3666
  // src/components/dictation.tsx
3278
3667
  /**
3279
3668
  * Voice dictation in the composer, similar to claude.ai's: while listening the words fill the box as they are
@@ -3773,7 +4162,7 @@ function EffortSlider({ efforts, value, onChange, label, className }) {
3773
4162
  function EffortButton({ icon, children, className, ...slider }) {
3774
4163
  const current = slider.efforts.find((e) => e.id === slider.value) ?? slider.efforts[0];
3775
4164
  const labels = useLabels();
3776
- return /* @__PURE__ */ jsxs(DialogTrigger, { children: [/* @__PURE__ */ jsxs(Button, {
4165
+ return /* @__PURE__ */ jsxs(DialogTrigger$1, { children: [/* @__PURE__ */ jsxs(Button, {
3777
4166
  variant: "ghost",
3778
4167
  size: "sm",
3779
4168
  className: clsx("text-chat-muted data-hovered:text-chat-fg", className),
@@ -3801,6 +4190,7 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
3801
4190
  const more = models.filter((m) => m.secondary);
3802
4191
  const item = (m) => /* @__PURE__ */ jsx(MenuItem, {
3803
4192
  id: m.id,
4193
+ ...m.icon && { icon: m.icon },
3804
4194
  description: m.disabledReason ?? m.description,
3805
4195
  isDisabled: m.disabled ?? false,
3806
4196
  children: m.label
@@ -3815,6 +4205,10 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
3815
4205
  "aria-label": labels.modelButton(current?.label ?? model),
3816
4206
  className: clsx("text-chat-muted data-hovered:text-chat-fg", className),
3817
4207
  children: [
4208
+ current?.icon && /* @__PURE__ */ jsx(IconSlot, {
4209
+ className: "text-chat-fg [&_svg]:size-3.5",
4210
+ children: current.icon
4211
+ }),
3818
4212
  /* @__PURE__ */ jsx("span", {
3819
4213
  className: "min-w-0 truncate text-chat-fg",
3820
4214
  children: current?.label ?? model
@@ -3918,10 +4312,12 @@ function MessageList({ messages, onRetry, onEdit, onFeedback, onReadAloud, actio
3918
4312
  behavior
3919
4313
  });
3920
4314
  }, []);
3921
- const onScroll = () => {
4315
+ const nearBottom = () => {
3922
4316
  const el = scroller.current;
3923
- if (!el) return;
3924
- const bottom = el.scrollHeight - el.scrollTop - el.clientHeight < STICK_SLOP;
4317
+ return !el || el.scrollHeight - el.scrollTop - el.clientHeight < STICK_SLOP;
4318
+ };
4319
+ const onScroll = () => {
4320
+ const bottom = nearBottom();
3925
4321
  stuck.current = bottom;
3926
4322
  setAtBottom(bottom);
3927
4323
  };
@@ -3938,7 +4334,7 @@ function MessageList({ messages, onRetry, onEdit, onFeedback, onReadAloud, actio
3938
4334
  const jumpedFor = useRef(start === "top" ? lastUser : null);
3939
4335
  useLayoutEffect(() => {
3940
4336
  if (lastUser === jumpedFor.current) {
3941
- onScroll();
4337
+ setAtBottom(nearBottom());
3942
4338
  return;
3943
4339
  }
3944
4340
  jumpedFor.current = lastUser;
@@ -4084,7 +4480,7 @@ function ChatTitleMenu({ pinned, onPin, onRename, projects, onAddToProject, onCr
4084
4480
  children: labels.shareChat
4085
4481
  }),
4086
4482
  children,
4087
- onDelete && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MenuSeparator, {}), /* @__PURE__ */ jsx(MenuItem, {
4483
+ onDelete && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(MenuSeparator, {}), /* @__PURE__ */ jsx(MenuItem, {
4088
4484
  id: "delete",
4089
4485
  icon: icons.delete,
4090
4486
  shortcut: "D",
@@ -4186,15 +4582,22 @@ function ChatRow({ chat, active, nested, onSelect, menu }) {
4186
4582
  role: "img",
4187
4583
  "aria-label": labels.running,
4188
4584
  className: clsx(markerClass, "animate-pulse bg-chat-accent motion-reduce:animate-none")
4189
- }) : chat.unread ? /* @__PURE__ */ jsx("span", {
4585
+ }) : chat.unread && !chat.muted ? /* @__PURE__ */ jsx("span", {
4190
4586
  role: "img",
4191
4587
  "aria-label": labels.unread,
4192
4588
  className: clsx(markerClass, "bg-chat-check")
4193
4589
  }) : 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] });
4590
+ const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [
4591
+ /* @__PURE__ */ jsx("span", {
4592
+ className: clsx("min-w-0 flex-1 truncate", chat.unread && !active && !chat.muted && "font-medium", chat.muted && "text-chat-muted"),
4593
+ children: chat.title
4594
+ }),
4595
+ chat.detail != null && /* @__PURE__ */ jsx("span", {
4596
+ className: "shrink-0 text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle tabular-nums",
4597
+ children: chat.detail
4598
+ }),
4599
+ marker
4600
+ ] });
4198
4601
  const common = {
4199
4602
  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
4603
  "aria-current": active ? "page" : void 0,
@@ -4231,7 +4634,7 @@ function ProjectRow({ project, activeId, row }) {
4231
4634
  const holdsActive = project.chats.some((c) => c.id === activeId);
4232
4635
  return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Disclosure$1, {
4233
4636
  defaultExpanded: project.defaultExpanded ?? holdsActive,
4234
- children: ({ isExpanded }) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Heading, {
4637
+ children: ({ isExpanded }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Heading, {
4235
4638
  className: "m-0 font-normal",
4236
4639
  children: /* @__PURE__ */ jsxs(Button$1, {
4237
4640
  slot: "trigger",
@@ -4290,6 +4693,7 @@ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove
4290
4693
  const labels = useLabels();
4291
4694
  const pending = onApprove && approvals?.[0];
4292
4695
  const box = (variant) => {
4696
+ if (composer === false) return null;
4293
4697
  const element = /* @__PURE__ */ jsx(Composer, {
4294
4698
  variant: density === "inline" || short && !empty ? "compact" : variant,
4295
4699
  busy,
@@ -4423,41 +4827,84 @@ function ChatTranscript({ messages, renderPart, widgets, header, notice, footer,
4423
4827
  });
4424
4828
  }
4425
4829
  // src/views/app-shell.tsx
4830
+ const DEFAULT_WIDTH = 288;
4831
+ const KEY_STEP = 16;
4832
+ const clampWidth = (px, min, max) => Math.min(max, Math.max(min, Math.round(px)));
4833
+ function readStoredWidth(key) {
4834
+ if (!key) return void 0;
4835
+ try {
4836
+ const n = Number(window.localStorage.getItem(key));
4837
+ return n > 0 ? n : void 0;
4838
+ } catch {
4839
+ return;
4840
+ }
4841
+ }
4426
4842
  /**
4427
4843
  * A page with a sidebar: pinned and collapsible to an icon rail from tablet width up, a drawer over the
4428
4844
  * content on phones. Children get `useSidebar()` and `<SidebarToggle>`.
4429
4845
  */
4430
- function AppShell({ sidebar, children, defaultCollapsed, className }) {
4846
+ function AppShell({ sidebar, children, defaultCollapsed, collapsed: collapsedProp, onCollapsedChange, scroll = "inner", resizable, width: widthProp, defaultWidth, onWidthChange, persistKey, shortcut = true, className }) {
4431
4847
  const drawer = useMediaQuery(DRAWER_QUERY);
4432
- const [collapsed, setCollapsed] = useState(() => defaultCollapsed ?? (typeof window !== "undefined" && window.matchMedia("(max-width: 1023.98px)").matches));
4848
+ const labels = useLabels();
4849
+ const [collapsed, setCollapsed] = useControllable(collapsedProp, () => defaultCollapsed ?? (typeof window !== "undefined" && window.matchMedia("(max-width: 1023.98px)").matches), onCollapsedChange);
4433
4850
  const [drawerOpen, setDrawerOpen] = useState(false);
4434
- const toggle = useCallback(() => drawer ? setDrawerOpen((o) => !o) : setCollapsed((c) => !c), [drawer]);
4851
+ const toggle = useCallback(() => drawer ? setDrawerOpen((o) => !o) : setCollapsed((c) => !c), [drawer, setCollapsed]);
4435
4852
  const dismiss = useCallback(() => setDrawerOpen(false), []);
4853
+ const min = resizable?.min ?? 0;
4854
+ const max = resizable?.max ?? Infinity;
4855
+ const [rawWidth, setWidth] = useControllable(widthProp, () => readStoredWidth(persistKey) ?? defaultWidth ?? DEFAULT_WIDTH, (px) => {
4856
+ if (persistKey) try {
4857
+ window.localStorage.setItem(persistKey, String(px));
4858
+ } catch {}
4859
+ onWidthChange?.(px);
4860
+ });
4861
+ const width = resizable ? clampWidth(rawWidth, min, max) : void 0;
4862
+ const shortcutKeys = shortcut ? `${isMacPlatform() ? "⌘" : labels.ctrlKey} B` : void 0;
4863
+ useEffect(() => {
4864
+ if (!shortcut) return;
4865
+ const mac = isMacPlatform();
4866
+ const onKey = (e) => {
4867
+ if (e.defaultPrevented || e.altKey || e.shiftKey || e.key.toLowerCase() !== "b") return;
4868
+ if (mac ? !e.metaKey || e.ctrlKey : !e.ctrlKey || e.metaKey) return;
4869
+ e.preventDefault();
4870
+ toggle();
4871
+ };
4872
+ window.addEventListener("keydown", onKey);
4873
+ return () => window.removeEventListener("keydown", onKey);
4874
+ }, [shortcut, toggle]);
4436
4875
  const pinned = useMemo(() => ({
4437
4876
  collapsed,
4438
4877
  drawer,
4439
4878
  drawerOpen,
4440
4879
  toggle,
4441
- dismiss
4880
+ dismiss,
4881
+ ...shortcutKeys && { shortcut: shortcutKeys }
4442
4882
  }), [
4443
4883
  collapsed,
4444
4884
  drawer,
4445
4885
  drawerOpen,
4446
4886
  toggle,
4447
- dismiss
4887
+ dismiss,
4888
+ shortcutKeys
4448
4889
  ]);
4449
- const labels = useLabels();
4450
4890
  const inDrawer = useMemo(() => ({
4451
4891
  ...pinned,
4452
4892
  collapsed: false
4453
4893
  }), [pinned]);
4894
+ const windowScroll = scroll === "window";
4454
4895
  return /* @__PURE__ */ jsxs(SidebarContext.Provider, {
4455
4896
  value: pinned,
4456
4897
  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
4898
+ className: clsx("flex bg-chat-bg text-chat-fg", windowScroll ? "min-h-dvh" : "h-dvh overflow-hidden", className),
4899
+ children: [!drawer && /* @__PURE__ */ jsxs("aside", {
4900
+ style: width ? { "--chat-sidebar-width": `${width}px` } : void 0,
4901
+ 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)"),
4902
+ children: [sidebar, resizable && !collapsed && width && /* @__PURE__ */ jsx(ResizeHandle, {
4903
+ width,
4904
+ min,
4905
+ max,
4906
+ onCommit: setWidth
4907
+ })]
4461
4908
  }), /* @__PURE__ */ jsx("main", {
4462
4909
  className: "flex min-w-0 flex-1 flex-col",
4463
4910
  children
@@ -4473,15 +4920,58 @@ function AppShell({ sidebar, children, defaultCollapsed, className }) {
4473
4920
  })]
4474
4921
  });
4475
4922
  }
4476
- /** Opens the drawer on phones, collapses / expands the pinned sidebar elsewhere. */
4923
+ /** The draggable right edge of the sidebar. A drag moves the aside's width variable alone and commits on release. */
4924
+ function ResizeHandle({ width, min, max, onCommit }) {
4925
+ const labels = useLabels();
4926
+ const live = useRef(null);
4927
+ const aside = (el) => el.closest("aside");
4928
+ const move = (el, px) => {
4929
+ live.current = clampWidth(px, min, max);
4930
+ aside(el).style.setProperty("--chat-sidebar-width", `${live.current}px`);
4931
+ };
4932
+ const end = (el) => {
4933
+ delete aside(el).dataset.resizing;
4934
+ if (live.current != null) onCommit(live.current);
4935
+ live.current = null;
4936
+ };
4937
+ return /* @__PURE__ */ jsx("div", {
4938
+ role: "separator",
4939
+ "aria-orientation": "vertical",
4940
+ "aria-label": labels.resizeSidebar,
4941
+ "aria-valuenow": width,
4942
+ "aria-valuemin": min,
4943
+ "aria-valuemax": max,
4944
+ tabIndex: 0,
4945
+ className: "group/handle absolute inset-y-0 right-0 z-10 w-1.5 cursor-col-resize touch-none outline-none",
4946
+ onPointerDown: (e) => {
4947
+ e.currentTarget.setPointerCapture(e.pointerId);
4948
+ aside(e.currentTarget).dataset.resizing = "";
4949
+ live.current = null;
4950
+ },
4951
+ onPointerMove: (e) => {
4952
+ if (e.currentTarget.hasPointerCapture(e.pointerId)) move(e.currentTarget, e.clientX - aside(e.currentTarget).getBoundingClientRect().left);
4953
+ },
4954
+ onPointerUp: (e) => end(e.currentTarget),
4955
+ onPointerCancel: (e) => end(e.currentTarget),
4956
+ onKeyDown: (e) => {
4957
+ const step = e.shiftKey ? 64 : KEY_STEP;
4958
+ const next = e.key === "ArrowLeft" ? width - step : e.key === "ArrowRight" ? width + step : e.key === "Home" ? min : e.key === "End" ? max : null;
4959
+ if (next == null) return;
4960
+ e.preventDefault();
4961
+ onCommit(clampWidth(next, min, max));
4962
+ },
4963
+ 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" })
4964
+ });
4965
+ }
4966
+ /** Opens the drawer on phones, collapses / expands the pinned sidebar elsewhere. Its label names the shortcut, e.g. "(⌘ B)". */
4477
4967
  function SidebarToggle({ drawerOnly = false, className }) {
4478
4968
  const icons = useIcons();
4479
- const { drawer, drawerOpen, collapsed, toggle } = useSidebar();
4969
+ const { drawer, drawerOpen, collapsed, toggle, shortcut } = useSidebar();
4480
4970
  const labels = useLabels();
4481
4971
  if (drawerOnly && !drawer) return null;
4482
4972
  const label = drawer ? drawerOpen ? labels.closeSidebar : labels.openSidebar : collapsed ? labels.expandSidebar : labels.collapseSidebar;
4483
4973
  return /* @__PURE__ */ jsx(IconButton, {
4484
- label,
4974
+ label: shortcut ? labels.withShortcut(label, shortcut) : label,
4485
4975
  onPress: toggle,
4486
4976
  className,
4487
4977
  children: icons.sidebar
@@ -4489,7 +4979,7 @@ function SidebarToggle({ drawerOnly = false, className }) {
4489
4979
  }
4490
4980
  // src/views/sidebar.tsx
4491
4981
  /** 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 }) {
4982
+ function Sidebar({ brand, brandIcon, headerActions, railActions, nav, children, footer, className }) {
4493
4983
  const { collapsed } = useSidebar();
4494
4984
  return /* @__PURE__ */ jsxs("div", {
4495
4985
  className: clsx("flex h-full flex-col font-chat-sans", collapsed ? "w-13" : "w-full", className),
@@ -4515,8 +5005,12 @@ function Sidebar({ brand, brandIcon, headerActions, nav, children, footer, class
4515
5005
  /* @__PURE__ */ jsx(SidebarToggle, {})
4516
5006
  ]
4517
5007
  }),
4518
- nav && /* @__PURE__ */ jsx("ul", {
4519
- className: clsx("flex shrink-0 flex-col gap-px px-2 pb-2", collapsed && "items-center"),
5008
+ collapsed && railActions && /* @__PURE__ */ jsx("div", {
5009
+ className: "flex shrink-0 flex-col items-center gap-1 pb-2",
5010
+ children: railActions
5011
+ }),
5012
+ nav && /* @__PURE__ */ jsx(SidebarList, {
5013
+ className: clsx("shrink-0 px-2 pb-2", collapsed && "items-center"),
4520
5014
  children: nav
4521
5015
  }),
4522
5016
  !collapsed && /* @__PURE__ */ jsx("div", {
@@ -4531,32 +5025,65 @@ function Sidebar({ brand, brandIcon, headerActions, nav, children, footer, class
4531
5025
  ]
4532
5026
  });
4533
5027
  }
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 }) {
5028
+ /** The `<ul>` for `SidebarItem`s outside `Sidebar`'s `nav` slot (e.g. inside a section): no bullets, no padding. */
5029
+ function SidebarList({ children, className }) {
5030
+ return /* @__PURE__ */ jsx("ul", {
5031
+ className: clsx("m-0 flex list-none flex-col gap-px p-0", className),
5032
+ children
5033
+ });
5034
+ }
5035
+ const iconSize = "[&_svg]:size-(--chat-sidebar-icon-size,1rem) [&_svg]:shrink-0";
5036
+ /**
5037
+ * A nav row: icon and label; just the icon with a tooltip when collapsed. Closes the drawer when picked.
5038
+ * Icons are 1rem, or `--chat-sidebar-icon-size`.
5039
+ */
5040
+ function SidebarItem({ icon, label, onPress, href, active, shortcut, badge, description, trailing, title, className: extra, ...rest }) {
4536
5041
  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");
5042
+ const data = Object.fromEntries(Object.entries(rest).filter(([k]) => k.startsWith("data-")));
5043
+ const hasTrailing = !collapsed && trailing != null;
5044
+ 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
5045
  const press = () => {
4539
5046
  onPress?.();
4540
5047
  dismiss();
4541
5048
  };
4542
- const content = /* @__PURE__ */ jsxs(Fragment, { children: [
5049
+ const showIfCut = (e) => {
5050
+ const el = e.currentTarget;
5051
+ el.title = el.scrollWidth > el.clientWidth ? label : "";
5052
+ };
5053
+ const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [
4543
5054
  icon,
4544
- collapsed ? /* @__PURE__ */ jsx("span", {
5055
+ collapsed ? /* @__PURE__ */ jsxs("span", {
4545
5056
  className: "sr-only",
4546
- children: label
4547
- }) : /* @__PURE__ */ jsx("span", {
4548
- className: "min-w-0 flex-1 truncate",
4549
- children: label
5057
+ children: [label, (typeof badge === "string" || typeof badge === "number") && ` (${badge})`]
5058
+ }) : /* @__PURE__ */ jsxs("span", {
5059
+ className: "flex min-w-0 flex-1 flex-col",
5060
+ children: [/* @__PURE__ */ jsx("span", {
5061
+ className: "truncate",
5062
+ onPointerEnter: title ? void 0 : showIfCut,
5063
+ children: label
5064
+ }), description && /* @__PURE__ */ jsx("span", {
5065
+ className: "truncate text-(length:--chat-ui-font-size-sm) leading-4 text-chat-muted",
5066
+ children: description
5067
+ })]
4550
5068
  }),
4551
5069
  !collapsed && shortcut && /* @__PURE__ */ jsx("kbd", {
4552
5070
  className: "font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle opacity-0 group-data-hovered:opacity-100",
4553
5071
  children: shortcut
5072
+ }),
5073
+ !collapsed && badge != null && /* @__PURE__ */ jsx("span", {
5074
+ className: "shrink-0 text-(length:--chat-ui-font-size-sm) leading-4 text-chat-muted tabular-nums",
5075
+ children: badge
5076
+ }),
5077
+ collapsed && badge != null && /* @__PURE__ */ jsx("span", {
5078
+ "aria-hidden": true,
5079
+ className: "absolute top-1 right-1 size-2 rounded-full bg-chat-accent"
4554
5080
  })
4555
5081
  ] });
4556
5082
  const common = {
4557
5083
  className,
4558
5084
  onPress: press,
4559
- "aria-current": active ? "page" : void 0
5085
+ "aria-current": active ? "page" : void 0,
5086
+ ...title && { title }
4560
5087
  };
4561
5088
  const control = href ? /* @__PURE__ */ jsx(Link, {
4562
5089
  ...common,
@@ -4566,11 +5093,18 @@ function SidebarItem({ icon, label, onPress, href, active, shortcut }) {
4566
5093
  ...common,
4567
5094
  children: content
4568
5095
  });
4569
- return /* @__PURE__ */ jsx("li", { children: collapsed ? /* @__PURE__ */ jsx(Tooltip, {
4570
- content: label,
4571
- placement: "right",
4572
- children: control
4573
- }) : control });
5096
+ return /* @__PURE__ */ jsxs("li", {
5097
+ ...data,
5098
+ className: clsx("group/item relative list-none", extra),
5099
+ children: [collapsed ? /* @__PURE__ */ jsx(Tooltip, {
5100
+ content: label,
5101
+ placement: "right",
5102
+ children: control
5103
+ }) : control, hasTrailing && /* @__PURE__ */ jsx("div", {
5104
+ 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",
5105
+ children: trailing
5106
+ })]
5107
+ });
4574
5108
  }
4575
5109
  const initials = (name) => name.split(/\s+/).slice(0, 2).map((w) => w[0]?.toUpperCase() ?? "").join("");
4576
5110
  /** The account row at the bottom: avatar, name, plan, and a menu. */
@@ -4589,7 +5123,7 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
4589
5123
  const button = /* @__PURE__ */ jsxs(Button$1, {
4590
5124
  "aria-label": collapsed ? name : void 0,
4591
5125
  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", {
5126
+ children: [avatar, !collapsed && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("span", {
4593
5127
  className: "flex min-w-0 flex-1 flex-col",
4594
5128
  children: [/* @__PURE__ */ jsx("span", {
4595
5129
  className: "truncate text-(length:--chat-ui-font-size) leading-5 text-chat-fg",
@@ -4609,4 +5143,4 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
4609
5143
  children: menu
4610
5144
  })] });
4611
5145
  }
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 };
5146
+ export { ActivityGroup, AgentProvider, AgentRegistry, AppShell, ApprovalCard, AssistantMessage, AttachButton, AttachmentCard, AttachmentRow, Badge, Button, Caret, ChatHeader, ChatList, 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 };