agentic-ui-kit 0.2.1 → 0.4.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.
Files changed (31) hide show
  1. package/README.md +109 -2
  2. package/dist/core.js +96 -1
  3. package/dist/index.js +1482 -304
  4. package/dist/styles.css +1 -1
  5. package/dist/tailwind.css +33 -3
  6. package/dist/types/core/src/index.d.ts +8 -1
  7. package/dist/types/core/src/partial-json.d.ts +8 -0
  8. package/dist/types/core/src/types.d.ts +5 -0
  9. package/dist/types/core/src/ui-markup.d.ts +92 -0
  10. package/dist/types/core/src/ui-tree.d.ts +110 -0
  11. package/dist/types/core/src/visualize.d.ts +60 -0
  12. package/dist/types/react/src/components/chat-notice.d.ts +11 -0
  13. package/dist/types/react/src/components/composer.d.ts +7 -1
  14. package/dist/types/react/src/components/markdown.d.ts +19 -1
  15. package/dist/types/react/src/components/message-list.d.ts +10 -2
  16. package/dist/types/react/src/components/messages.d.ts +25 -4
  17. package/dist/types/react/src/components/tools.d.ts +20 -7
  18. package/dist/types/react/src/components/ui-tree.d.ts +87 -0
  19. package/dist/types/react/src/components/widget-frame.d.ts +97 -0
  20. package/dist/types/react/src/index.d.ts +23 -4
  21. package/dist/types/react/src/primitives/button.d.ts +10 -1
  22. package/dist/types/react/src/primitives/chat-theme.d.ts +13 -1
  23. package/dist/types/react/src/primitives/icons.d.ts +95 -0
  24. package/dist/types/react/src/primitives/labels.d.ts +193 -0
  25. package/dist/types/react/src/primitives/menu.d.ts +3 -1
  26. package/dist/types/react/src/primitives/segmented-control.d.ts +22 -0
  27. package/dist/types/react/src/views/chat-transcript.d.ts +27 -0
  28. package/dist/types/react/src/views/chat-view.d.ts +20 -3
  29. package/dist/ui-markup-DtMYer7I.js +1082 -0
  30. package/package.json +1 -1
  31. package/dist/sse-BA_ebMcH.js +0 -408
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  "use client";
2
- import { a as activityTokens, f as settleToolCall, i as toBlocks, n as readEventStream, o as applyEvent, s as awaitingToolCalls } from "./sse-BA_ebMcH.js";
2
+ import { A as salvageJson, C as awaitingToolCalls, O as settleToolCall, S as applyEvent, b as toBlocks, d as isStateRef, f as matchesSchema, g as uiActionSchema, i as parseUiMarkup, v as readEventStream, x as activityTokens } from "./ui-markup-DtMYer7I.js";
3
3
  import { clsx } from "clsx";
4
- import { Autocomplete, Button as Button$1, Dialog, Disclosure as Disclosure$1, DisclosurePanel, Focusable, Header, Heading, Input, Keyboard, Link, Menu as Menu$1, MenuItem as MenuItem$1, MenuSection as MenuSection$1, MenuTrigger, MenuTrigger as MenuTrigger$1, Modal as Modal$1, ModalOverlay, Popover as Popover$1, SearchField, Separator, SubmenuTrigger, Text, Tooltip as Tooltip$1, TooltipTrigger, useFilter } from "react-aria-components";
4
+ import { Autocomplete, Button as Button$1, Dialog, Disclosure as Disclosure$1, DisclosurePanel, Focusable, Header, Heading, Input, Keyboard, Link, Menu as Menu$1, MenuItem as MenuItem$1, MenuSection as MenuSection$1, MenuTrigger, MenuTrigger as MenuTrigger$1, Modal as Modal$1, ModalOverlay, Popover as Popover$1, SearchField, Separator, SubmenuTrigger, Text, ToggleButton, ToggleButtonGroup, Tooltip as Tooltip$1, TooltipTrigger, useFilter } from "react-aria-components";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
- import { AlertTriangle, ArrowDown, ArrowUp, Check, ChevronDown, ChevronRight, ChevronsUpDown, Copy, FileText, Folder, FolderOpen, FolderPlus, Globe, MoreHorizontal, PanelLeft, Pencil, Pin, PinOff, Plus, RefreshCw, Share, ShieldQuestion, Square, ThumbsDown, ThumbsUp, Trash2, Volume2, X } from "lucide-react";
7
- import { Fragment as Fragment$1, createContext, createElement, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
6
+ import { AlertTriangle, ArrowDown, ArrowUp, Check, ChevronDown, ChevronRight, ChevronsUpDown, Copy, FileText, Folder, FolderOpen, FolderPlus, Globe, Info, MoreHorizontal, PanelLeft, Pencil, Pin, PinOff, Plus, RefreshCw, Share, ShieldQuestion, Square, ThumbsDown, ThumbsUp, Trash2, Volume2, X } from "lucide-react";
7
+ import { Component, Fragment as Fragment$1, createContext, createElement, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
8
8
  import { UNSAFE_PortalProvider } from "react-aria";
9
9
  import { createMathPlugin } from "@streamdown/math";
10
10
  import { Streamdown, defaultRehypePlugins } from "streamdown";
@@ -16,7 +16,7 @@ function Tooltip({ content, children, placement = "top", delay = 600 }) {
16
16
  children: [children, /* @__PURE__ */ jsx(Tooltip$1, {
17
17
  placement,
18
18
  offset: 6,
19
- className: "rounded-chat-sm bg-chat-raised px-2 py-1 font-chat-sans text-xs text-chat-fg shadow-chat-popover motion-safe:data-entering:animate-chat-in motion-safe:data-exiting:animate-chat-out",
19
+ className: "rounded-chat-sm bg-chat-raised px-2 py-1 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-fg shadow-chat-popover motion-safe:data-entering:animate-chat-in motion-safe:data-exiting:animate-chat-out",
20
20
  children: content
21
21
  })]
22
22
  });
@@ -27,25 +27,34 @@ const variants = {
27
27
  /** Plain text button, highlights on hover. Sidebar rows, header buttons. */
28
28
  ghost: "text-chat-fg data-hovered:bg-chat-hover data-pressed:bg-chat-selected",
29
29
  /** Filled with the surface color, e.g. the selected Chat/Cowork toggle. */
30
- subtle: "bg-chat-surface text-chat-fg data-hovered:bg-chat-hover data-pressed:bg-chat-selected",
31
- /** The send button. */
32
- primary: "bg-chat-accent text-white data-hovered:brightness-110 data-pressed:brightness-95"
30
+ subtle: "bg-chat-surface font-medium text-chat-fg data-hovered:bg-chat-hover data-pressed:bg-chat-selected",
31
+ /** A thin border, no fill: secondary actions next to a primary one, e.g. Cancel or Share. */
32
+ outline: "border border-chat-border-strong font-medium text-chat-fg data-hovered:bg-chat-hover data-pressed:bg-chat-selected",
33
+ /** The send button and the one main action of a dialog. */
34
+ primary: "bg-chat-accent font-medium text-chat-accent-fg data-hovered:brightness-110 data-pressed:brightness-95",
35
+ /**
36
+ * Destructive confirmations, e.g. Delete chat. Fill --chat-danger-bg (default: --chat-danger, darkened so it
37
+ * doesn't read as the accent), text --chat-danger-fg (default white).
38
+ */
39
+ danger: "bg-[var(--chat-danger-bg,color-mix(in_srgb,var(--chat-danger)_70%,black))] font-medium text-[var(--chat-danger-fg,#fff)] data-hovered:brightness-110 data-pressed:brightness-95"
33
40
  };
34
41
  const sizes = {
35
- sm: "h-7 gap-1.5 px-2 text-sm [@media(pointer:coarse)]:h-9",
36
- md: "h-8 gap-2 px-2.5 text-sm [@media(pointer:coarse)]:h-10"
42
+ sm: "h-7 gap-1.5 px-2 text-(length:--chat-ui-font-size) leading-5 [@media(pointer:coarse)]:h-9",
43
+ md: "h-8 gap-2 px-2.5 text-(length:--chat-ui-font-size) leading-5 [@media(pointer:coarse)]:h-10",
44
+ /** Dialog and empty-state actions. */
45
+ lg: "h-9 gap-2 px-3.5 text-(length:--chat-ui-font-size) leading-5 [@media(pointer:coarse)]:h-11"
37
46
  };
38
47
  function Button({ variant = "ghost", size = "md", className, ...props }) {
39
48
  return /* @__PURE__ */ jsx(Button$1, {
40
49
  ...props,
41
- className: clsx("inline-flex cursor-pointer items-center rounded-chat-sm font-chat-sans whitespace-nowrap transition-colors select-none", "data-disabled:cursor-default data-disabled:opacity-40", focusRing, variants[variant], sizes[size], className)
50
+ className: clsx("inline-flex cursor-pointer items-center rounded-chat-sm font-chat-sans whitespace-nowrap transition-colors select-none [&_svg]:size-4 [&_svg]:shrink-0", "data-disabled:cursor-default data-disabled:opacity-40", focusRing, variants[variant], sizes[size], className)
42
51
  });
43
52
  }
44
53
  function IconButton({ label, variant = "ghost", size = "md", tooltip = true, className, ...props }) {
45
54
  const button = /* @__PURE__ */ jsx(Button$1, {
46
55
  ...props,
47
56
  "aria-label": label,
48
- className: clsx("inline-flex shrink-0 cursor-pointer items-center justify-center rounded-chat-sm text-chat-muted transition-colors select-none", "data-hovered:text-chat-fg data-disabled:cursor-default data-disabled:opacity-40 [&_svg]:size-4", focusRing, variants[variant], variant === "ghost" && "text-chat-muted", size === "sm" ? "size-7 [@media(pointer:coarse)]:size-9" : "size-8 [@media(pointer:coarse)]:size-10", className)
57
+ className: clsx("inline-flex shrink-0 cursor-pointer items-center justify-center rounded-chat-sm transition-colors select-none", "data-disabled:cursor-default data-disabled:opacity-40 [&_svg]:size-4", focusRing, variants[variant], variant === "ghost" && "text-chat-muted data-hovered:text-chat-fg", size === "sm" ? "size-7 [@media(pointer:coarse)]:size-9" : "size-8 [@media(pointer:coarse)]:size-10", className)
49
58
  });
50
59
  return tooltip ? /* @__PURE__ */ jsx(Tooltip, {
51
60
  content: label,
@@ -61,10 +70,41 @@ const tones = {
61
70
  };
62
71
  function Badge({ children, tone = "outline", icon, className }) {
63
72
  return /* @__PURE__ */ jsxs("span", {
64
- className: clsx("inline-flex items-center gap-1 rounded-full px-2 py-px font-chat-sans text-xs whitespace-nowrap [&_svg]:size-3", tones[tone], className),
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),
65
74
  children: [icon, children]
66
75
  });
67
76
  }
77
+ // src/primitives/segmented-control.tsx
78
+ /**
79
+ * One of a few options, side by side on a track, like claude.ai's Chat / Cowork switch. Themed with
80
+ * `--chat-segment-track` (default: a shade darker than the surface it sits on) and `--chat-segment-selected`
81
+ * (default `--chat-raised`).
82
+ */
83
+ function SegmentedControl({ label, options, value, onChange, className }) {
84
+ return /* @__PURE__ */ jsx(ToggleButtonGroup, {
85
+ "aria-label": label,
86
+ selectionMode: "single",
87
+ disallowEmptySelection: true,
88
+ selectedKeys: [value],
89
+ onSelectionChange: (keys) => {
90
+ const next = [...keys][0];
91
+ if (next !== void 0) onChange(next);
92
+ },
93
+ 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),
94
+ children: options.map((o) => {
95
+ const button = /* @__PURE__ */ jsxs(ToggleButton, {
96
+ id: o.id,
97
+ ...o.iconOnly && { "aria-label": o.label },
98
+ 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)]"),
99
+ children: [o.icon, !o.iconOnly && o.label]
100
+ }, o.id);
101
+ return o.iconOnly ? /* @__PURE__ */ jsx(Tooltip, {
102
+ content: o.label,
103
+ children: button
104
+ }, o.id) : button;
105
+ })
106
+ });
107
+ }
68
108
  // src/primitives/caret.tsx
69
109
  /** The small filled triangle claude.ai uses after expandable labels. Points right; rotate to point down. */
70
110
  function Caret({ className }) {
@@ -75,9 +115,77 @@ function Caret({ className }) {
75
115
  children: /* @__PURE__ */ jsx("path", { d: "M2 1.2 6.2 4 2 6.8z" })
76
116
  });
77
117
  }
118
+ // src/primitives/icons.tsx
119
+ /** Lucide icons, plus the claude.ai caret. Unsized: the places that show them size them with CSS. */
120
+ const defaultIcons = {
121
+ copy: /* @__PURE__ */ jsx(Copy, {}),
122
+ copied: /* @__PURE__ */ jsx(Check, {}),
123
+ retry: /* @__PURE__ */ jsx(RefreshCw, {}),
124
+ edit: /* @__PURE__ */ jsx(Pencil, {}),
125
+ speak: /* @__PURE__ */ jsx(Volume2, {}),
126
+ thumbsUp: /* @__PURE__ */ jsx(ThumbsUp, {}),
127
+ thumbsDown: /* @__PURE__ */ jsx(ThumbsDown, {}),
128
+ send: /* @__PURE__ */ jsx(ArrowUp, {}),
129
+ stop: /* @__PURE__ */ jsx(Square, {}),
130
+ attach: /* @__PURE__ */ jsx(Plus, {}),
131
+ scrollToBottom: /* @__PURE__ */ jsx(ArrowDown, {}),
132
+ tokens: /* @__PURE__ */ jsx(ArrowDown, {}),
133
+ sidebar: /* @__PURE__ */ jsx(PanelLeft, {}),
134
+ switcher: /* @__PURE__ */ jsx(ChevronsUpDown, {}),
135
+ dropdown: /* @__PURE__ */ jsx(ChevronDown, {}),
136
+ expand: /* @__PURE__ */ jsx(ChevronRight, {}),
137
+ caret: /* @__PURE__ */ jsx(Caret, {}),
138
+ submenu: /* @__PURE__ */ jsx(ChevronRight, {}),
139
+ check: /* @__PURE__ */ jsx(Check, {}),
140
+ info: /* @__PURE__ */ jsx(Info, {}),
141
+ warning: /* @__PURE__ */ jsx(AlertTriangle, {}),
142
+ approval: /* @__PURE__ */ jsx(ShieldQuestion, {}),
143
+ web: /* @__PURE__ */ jsx(Globe, {}),
144
+ pin: /* @__PURE__ */ jsx(Pin, {}),
145
+ unpin: /* @__PURE__ */ jsx(PinOff, {}),
146
+ rename: /* @__PURE__ */ jsx(Pencil, {}),
147
+ addToProject: /* @__PURE__ */ jsx(FolderPlus, {}),
148
+ add: /* @__PURE__ */ jsx(Plus, {}),
149
+ share: /* @__PURE__ */ jsx(Share, {}),
150
+ delete: /* @__PURE__ */ jsx(Trash2, {}),
151
+ more: /* @__PURE__ */ jsx(MoreHorizontal, {}),
152
+ folder: /* @__PURE__ */ jsx(Folder, {}),
153
+ folderOpen: /* @__PURE__ */ jsx(FolderOpen, {}),
154
+ file: /* @__PURE__ */ jsx(FileText, {}),
155
+ remove: /* @__PURE__ */ jsx(X, {})
156
+ };
157
+ const IconsContext = createContext(defaultIcons);
158
+ /** Merges `icons` over the icons of the enclosing provider (or the defaults). */
159
+ function IconsProvider({ icons, children }) {
160
+ const parent = useIcons();
161
+ const merged = useMemo(() => icons ? {
162
+ ...parent,
163
+ ...icons
164
+ } : parent, [parent, icons]);
165
+ return /* @__PURE__ */ jsx(IconsContext.Provider, {
166
+ value: merged,
167
+ children
168
+ });
169
+ }
170
+ /** The active icons: the enclosing `ChatTheme`'s `icons` over the defaults. */
171
+ function useIcons() {
172
+ return useContext(IconsContext);
173
+ }
174
+ /**
175
+ * Sizes and colors an icon where no button does it already. Pass the size as `[&_svg]:size-4` so it
176
+ * reaches whatever SVG the host supplies.
177
+ */
178
+ function IconSlot({ children, className }) {
179
+ return /* @__PURE__ */ jsx("span", {
180
+ "aria-hidden": true,
181
+ className: clsx("inline-flex shrink-0 items-center justify-center", className),
182
+ children
183
+ });
184
+ }
78
185
  // src/primitives/disclosure.tsx
79
186
  /** A row that expands to show more, like "Recalled memory ▸". */
80
187
  function Disclosure({ summary, children, className, summaryClassName, panelClassName, indicator = "caret", trailing, ...props }) {
188
+ const icons = useIcons();
81
189
  return /* @__PURE__ */ jsxs(Disclosure$1, {
82
190
  ...props,
83
191
  ...className !== void 0 && { className },
@@ -87,15 +195,18 @@ function Disclosure({ summary, children, className, summaryClassName, panelClass
87
195
  slot: "trigger",
88
196
  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),
89
197
  children: [
90
- indicator === "chevron" && /* @__PURE__ */ jsx(ChevronRight, {
91
- "aria-hidden": true,
92
- className: "size-3.5 shrink-0 transition-transform group-aria-expanded/trigger:rotate-90"
198
+ indicator === "chevron" && /* @__PURE__ */ jsx(IconSlot, {
199
+ className: "transition-transform group-aria-expanded/trigger:rotate-90 [&_svg]:size-3.5",
200
+ children: icons.expand
93
201
  }),
94
202
  /* @__PURE__ */ jsx("span", {
95
203
  className: "min-w-0 truncate",
96
204
  children: summary
97
205
  }),
98
- indicator === "caret" && /* @__PURE__ */ jsx(Caret, { className: "group-aria-expanded/trigger:rotate-90" }),
206
+ indicator === "caret" && /* @__PURE__ */ jsx(IconSlot, {
207
+ className: "transition-transform duration-150 group-aria-expanded/trigger:rotate-90",
208
+ children: icons.caret
209
+ }),
99
210
  trailing && /* @__PURE__ */ jsx("span", {
100
211
  className: "ml-auto shrink-0 pl-2",
101
212
  children: trailing
@@ -122,7 +233,7 @@ function MenuSearch({ placeholder, children, value, onChange }) {
122
233
  className: "border-b border-chat-popover-border",
123
234
  children: /* @__PURE__ */ jsx(Input, {
124
235
  placeholder,
125
- className: "w-full bg-transparent px-3.5 py-2.5 text-sm text-chat-fg outline-none placeholder:text-chat-subtle"
236
+ className: "w-full bg-transparent px-3.5 py-2.5 text-(length:--chat-ui-font-size) leading-5 text-chat-fg outline-none placeholder:text-chat-subtle"
126
237
  })
127
238
  }), children]
128
239
  });
@@ -141,44 +252,45 @@ function Menu({ className, ...props }) {
141
252
  className: clsx("max-h-[inherit] min-w-52 overflow-auto p-1.5 outline-none", className)
142
253
  });
143
254
  }
144
- function MenuItem({ children, icon, description, detail, shortcut, tone = "default", className, ...props }) {
255
+ function MenuItem({ children, icon, description, descriptionPlacement = "below", detail, shortcut, tone = "default", className, ...props }) {
256
+ const icons = useIcons();
145
257
  const textValue = props.textValue ?? (typeof children === "string" ? children : void 0);
146
258
  return /* @__PURE__ */ jsx(MenuItem$1, {
147
259
  ...props,
148
260
  ...textValue !== void 0 && { textValue },
149
- className: clsx("group flex min-h-7 cursor-pointer items-center gap-2.5 rounded-chat-sm px-2 py-1 text-sm 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),
261
+ 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),
150
262
  children: ({ isSelected, hasSubmenu, selectionMode }) => /* @__PURE__ */ jsxs(Fragment, { children: [
151
263
  icon && /* @__PURE__ */ jsx("span", {
152
264
  className: clsx("flex size-4 shrink-0 items-center [&_svg]:size-4", tone === "danger" ? "text-chat-danger" : "text-chat-fg"),
153
265
  children: icon
154
266
  }),
155
267
  /* @__PURE__ */ jsxs("span", {
156
- className: "flex min-w-0 flex-1 flex-col",
268
+ className: clsx("flex min-w-0 flex-1", descriptionPlacement === "beside" ? "items-baseline gap-2" : "flex-col"),
157
269
  children: [/* @__PURE__ */ jsx(Text, {
158
270
  slot: "label",
159
- className: "truncate",
271
+ className: clsx("truncate", descriptionPlacement === "beside" && "shrink-0"),
160
272
  children
161
273
  }), description && /* @__PURE__ */ jsx(Text, {
162
274
  slot: "description",
163
- className: "text-xs text-chat-muted",
275
+ 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"),
164
276
  children: description
165
277
  })]
166
278
  }),
167
279
  detail && /* @__PURE__ */ jsx("span", {
168
- className: "shrink-0 text-sm text-chat-muted",
280
+ className: "shrink-0 text-(length:--chat-ui-font-size) leading-5 text-chat-muted",
169
281
  children: detail
170
282
  }),
171
283
  shortcut && /* @__PURE__ */ jsx(Keyboard, {
172
- className: "shrink-0 font-chat-sans text-xs text-chat-subtle",
284
+ className: "shrink-0 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle",
173
285
  children: shortcut
174
286
  }),
175
- selectionMode !== "none" && isSelected && /* @__PURE__ */ jsx(Check, {
176
- "aria-hidden": true,
177
- className: "size-4 shrink-0 text-chat-check"
287
+ selectionMode !== "none" && isSelected && /* @__PURE__ */ jsx(IconSlot, {
288
+ className: "text-chat-check [&_svg]:size-4",
289
+ children: icons.check
178
290
  }),
179
- hasSubmenu && /* @__PURE__ */ jsx(ChevronRight, {
180
- "aria-hidden": true,
181
- className: "size-4 shrink-0 text-chat-muted"
291
+ hasSubmenu && /* @__PURE__ */ jsx(IconSlot, {
292
+ className: "text-chat-muted [&_svg]:size-4",
293
+ children: icons.submenu
182
294
  })
183
295
  ] })
184
296
  });
@@ -188,7 +300,7 @@ function MenuSection({ title, children, ...selection }) {
188
300
  ...selection,
189
301
  className: "[&:not(:first-child)]:mt-1",
190
302
  children: [title && /* @__PURE__ */ jsx(Header, {
191
- className: "px-2 pt-1.5 pb-1 text-xs text-chat-subtle",
303
+ className: "px-2 pt-1.5 pb-1 text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle",
192
304
  children: title
193
305
  }), children]
194
306
  });
@@ -199,7 +311,7 @@ function MenuSeparator() {
199
311
  /** Explanatory text inside a menu, above or below the items. Not focusable. */
200
312
  function MenuNote({ children, className }) {
201
313
  return /* @__PURE__ */ jsx("div", {
202
- className: clsx("px-2 py-1.5 text-sm leading-snug text-chat-muted", className),
314
+ className: clsx("px-2 py-1.5 text-(length:--chat-ui-font-size) leading-snug text-chat-muted", className),
203
315
  children
204
316
  });
205
317
  }
@@ -260,7 +372,7 @@ function Modal({ isOpen, onOpenChange, title, children, className, ...props }) {
260
372
  isOpen,
261
373
  onOpenChange,
262
374
  isDismissable: true,
263
- className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4 data-entering:animate-chat-fade-in data-exiting:animate-chat-fade-out",
375
+ 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",
264
376
  children: /* @__PURE__ */ jsx(Modal$1, {
265
377
  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),
266
378
  children: /* @__PURE__ */ jsxs(Dialog, {
@@ -281,7 +393,7 @@ function Drawer({ isOpen, onOpenChange, side = "left", children, className, ...p
281
393
  isOpen,
282
394
  onOpenChange,
283
395
  isDismissable: true,
284
- className: "fixed inset-0 z-40 bg-black/50 data-entering:animate-chat-fade-in data-exiting:animate-chat-fade-out",
396
+ className: "fixed inset-0 z-40 bg-(--chat-backdrop) data-entering:animate-chat-fade-in data-exiting:animate-chat-fade-out",
285
397
  children: /* @__PURE__ */ jsx(Modal$1, {
286
398
  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),
287
399
  children: /* @__PURE__ */ jsx(Dialog, {
@@ -303,7 +415,7 @@ function Panel({ title, actions, footer, children, variant = "card", className,
303
415
  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),
304
416
  children: [
305
417
  (title || actions) && /* @__PURE__ */ jsxs("header", {
306
- className: "flex h-10 shrink-0 items-center gap-2 border-b border-chat-divider px-3 text-sm",
418
+ 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",
307
419
  children: [/* @__PURE__ */ jsx("div", {
308
420
  className: "min-w-0 flex-1 truncate font-medium",
309
421
  children: title
@@ -460,10 +572,10 @@ function CodeBlock({ code, language, label = language, variant = "plain", classN
460
572
  style: themeBackground ? result.vars : void 0,
461
573
  className: clsx(variant === "boxed" && "my-4 rounded-chat-md border border-chat-code-border bg-chat-code-bg px-4 py-3.5", className),
462
574
  children: [label && /* @__PURE__ */ jsx("div", {
463
- className: "mb-3 font-chat-sans text-xs text-chat-subtle",
575
+ className: "mb-3 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle",
464
576
  children: label
465
577
  }), /* @__PURE__ */ jsx("pre", {
466
- className: clsx("overflow-auto font-chat-mono text-[0.8125rem] leading-[1.75] whitespace-pre text-chat-fg", variant === "plain" && "max-h-56", "[scrollbar-color:var(--chat-border-strong)_transparent] [scrollbar-width:thin]"),
578
+ 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]"),
467
579
  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", {
468
580
  className: "chat-code-token",
469
581
  style: token.htmlStyle,
@@ -472,13 +584,130 @@ function CodeBlock({ code, language, label = language, variant = "plain", classN
472
584
  })]
473
585
  });
474
586
  }
587
+ // src/primitives/labels.tsx
588
+ const plural = (count, one, many) => count === 1 ? one : many(count);
589
+ /** The English strings. */
590
+ const defaultLabels = {
591
+ thinking: "Thinking…",
592
+ thought: "Thought",
593
+ thoughtFor: (duration) => `Thought for ${duration}`,
594
+ waitingForYou: " · waiting for you",
595
+ usedTool: (name) => `used ${name}`,
596
+ joinSummaries: (phrases) => {
597
+ const text = phrases.join(", ");
598
+ return text.charAt(0).toUpperCase() + text.slice(1);
599
+ },
600
+ duration: (seconds) => seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`,
601
+ ranCommandLabel: "Ran a command",
602
+ ranCommands: (count) => plural(count, "ran a command", (n) => `ran ${n} commands`),
603
+ searchedWebLabel: "Searched the web",
604
+ readPageLabel: "Read page",
605
+ readPages: (count) => plural(count, "read a page", (n) => `read ${n} pages`),
606
+ searchedWeb: (count) => plural(count, "searched the web", (n) => `searched the web ${n} times`),
607
+ recalledMemoryLabel: "Recalled memory",
608
+ recalledMemory: () => "recalled memory",
609
+ output: "Output",
610
+ noOutput: "(no output)",
611
+ noResults: "No results",
612
+ reasoningHidden: "The model's reasoning is not shown.",
613
+ allowTool: (name) => `Allow ${name}?`,
614
+ allowAction: (label) => /* @__PURE__ */ jsxs(Fragment, { children: [
615
+ "Allow ",
616
+ label,
617
+ "?"
618
+ ] }),
619
+ allow: "Allow",
620
+ deny: "Deny",
621
+ sidebar: "Sidebar",
622
+ closeSidebar: "Close sidebar",
623
+ openSidebar: "Open sidebar",
624
+ expandSidebar: "Expand sidebar",
625
+ collapseSidebar: "Collapse sidebar",
626
+ context: "Context",
627
+ contextUsed: "Context used",
628
+ percentUsed: (percent) => `${percent}% used`,
629
+ freeSpace: "Free space",
630
+ tokenCount: (count) => `${count} tokens`,
631
+ replyPlaceholder: "Reply…",
632
+ replyDirectly: "Or reply directly…",
633
+ message: "Message",
634
+ stop: "Stop",
635
+ send: "Send",
636
+ addFiles: "Add files and more",
637
+ attachments: "Attachments",
638
+ editPastedText: "Edit pasted text",
639
+ viewPastedText: "View pasted text",
640
+ removeAttachment: (name) => `Remove ${name}`,
641
+ pastedText: "Pasted text",
642
+ pasted: "Pasted",
643
+ failed: "Failed",
644
+ file: "File",
645
+ uploadFailed: "Upload failed",
646
+ cancel: "Cancel",
647
+ close: "Close",
648
+ save: "Save",
649
+ chats: "Chats",
650
+ running: "Running",
651
+ unread: "Unread",
652
+ moreOptionsFor: (title) => `More options for ${title}`,
653
+ new: "New",
654
+ viewAll: "View all",
655
+ chatActions: "Chat actions",
656
+ pin: "Pin",
657
+ unpin: "Unpin",
658
+ rename: "Rename",
659
+ addToProject: "Add to project",
660
+ searchProjects: "Search or create a project",
661
+ projects: "Projects",
662
+ newProject: "New project",
663
+ shareChat: "Share chat",
664
+ delete: "Delete",
665
+ justNow: "just now",
666
+ copy: "Copy",
667
+ copied: "Copied",
668
+ retry: "Retry",
669
+ edit: "Edit",
670
+ readAloud: "Read aloud",
671
+ goodResponse: "Good response",
672
+ badResponse: "Bad response",
673
+ somethingWentWrong: "Something went wrong.",
674
+ conversation: "Conversation",
675
+ scrollToBottom: "Scroll to bottom",
676
+ showMore: "Show more",
677
+ showLess: "Show less",
678
+ model: "Model",
679
+ modelButton: (label) => `Model: ${label}`,
680
+ effort: "Effort",
681
+ effortNote: "Higher effort means more thorough responses, but takes longer and uses your limits faster.",
682
+ recommended: "Recommended",
683
+ moreModels: "More models",
684
+ widget: "Widget",
685
+ drawingWidget: "Drawing"
686
+ };
687
+ const LabelsContext = createContext(defaultLabels);
688
+ /** Merges `labels` over the labels of the enclosing provider (or the defaults). */
689
+ function LabelsProvider({ labels, children }) {
690
+ const parent = useLabels();
691
+ const merged = useMemo(() => labels ? {
692
+ ...parent,
693
+ ...labels
694
+ } : parent, [parent, labels]);
695
+ return /* @__PURE__ */ jsx(LabelsContext.Provider, {
696
+ value: merged,
697
+ children
698
+ });
699
+ }
700
+ /** The active UI strings: the enclosing `ChatTheme`'s `labels` over the English defaults. */
701
+ function useLabels() {
702
+ return useContext(LabelsContext);
703
+ }
475
704
  // src/primitives/chat-theme.tsx
476
705
  /**
477
706
  * Applies a chat theme to part of a page. Menus, tooltips and dialogs opened inside render into this
478
707
  * element instead of `<body>`, so they get the same theme. An app themed as a whole (variables on
479
708
  * `:root`) does not need it for colors, but still uses it for `codeThemes` and `motion`.
480
709
  */
481
- function ChatTheme({ theme, codeThemes, motion, children, className }) {
710
+ function ChatTheme({ theme, codeThemes, motion, labels, icons, children, className }) {
482
711
  const portal = useRef(null);
483
712
  const getContainer = useCallback(() => portal.current, []);
484
713
  return /* @__PURE__ */ jsxs("div", {
@@ -488,9 +717,15 @@ function ChatTheme({ theme, codeThemes, motion, children, className }) {
488
717
  children: [/* @__PURE__ */ jsx(AppearanceProvider, {
489
718
  codeThemes,
490
719
  motion,
491
- children: /* @__PURE__ */ jsx(UNSAFE_PortalProvider, {
492
- getContainer,
493
- children
720
+ children: /* @__PURE__ */ jsx(LabelsProvider, {
721
+ labels,
722
+ children: /* @__PURE__ */ jsx(IconsProvider, {
723
+ icons,
724
+ children: /* @__PURE__ */ jsx(UNSAFE_PortalProvider, {
725
+ getContainer,
726
+ children
727
+ })
728
+ })
494
729
  })
495
730
  }), /* @__PURE__ */ jsx("div", { ref: portal })]
496
731
  });
@@ -1012,6 +1247,700 @@ const SidebarContext = createContext({
1012
1247
  dismiss: () => {}
1013
1248
  });
1014
1249
  const useSidebar = () => useContext(SidebarContext);
1250
+ // src/components/widget-frame.tsx
1251
+ const WidgetHostContext = createContext({});
1252
+ /** Sets what widgets below may do. ChatView and ChatTranscript set it from their `widgets` prop. */
1253
+ function WidgetHostProvider({ host, children }) {
1254
+ return /* @__PURE__ */ jsx(WidgetHostContext.Provider, {
1255
+ value: host,
1256
+ children
1257
+ });
1258
+ }
1259
+ function useWidgetHost() {
1260
+ return useContext(WidgetHostContext);
1261
+ }
1262
+ /** The tallest a widget may grow, in px. */
1263
+ const WIDGET_MAX_HEIGHT = 2e3;
1264
+ const PROTOCOL_VERSION = "2026-01-26";
1265
+ const THEME_VARS = [
1266
+ "bg",
1267
+ "fg",
1268
+ "muted",
1269
+ "subtle",
1270
+ "surface",
1271
+ "raised",
1272
+ "hover",
1273
+ "selected",
1274
+ "border",
1275
+ "border-strong",
1276
+ "divider",
1277
+ "accent",
1278
+ "accent-fg",
1279
+ "check",
1280
+ "warning",
1281
+ "danger",
1282
+ "code-bg",
1283
+ "font-sans",
1284
+ "font-serif",
1285
+ "font-mono",
1286
+ "radius-sm",
1287
+ "radius-md",
1288
+ "radius-lg",
1289
+ "ui-font-size",
1290
+ "ui-font-size-sm",
1291
+ "message-font-size"
1292
+ ];
1293
+ const ORIGIN = /^(https?|wss?):\/\/(\*\.)?[a-z0-9-]+(\.[a-z0-9-]+)*(:\d{1,5})?$/;
1294
+ function origins(list) {
1295
+ return (list ?? []).map((d) => d.trim().toLowerCase().replace(/\/$/, "")).filter((d) => ORIGIN.test(d));
1296
+ }
1297
+ /** Whether `origin` is covered by `allowed`, which may be a wildcard like "https://*.example.com". */
1298
+ function covers(allowed, origin) {
1299
+ if (allowed === origin) return true;
1300
+ const wild = /^([a-z]+:\/\/)\*\.(.+)$/.exec(allowed);
1301
+ return !!wild && origin.startsWith(wild[1]) && origin.slice(wild[1].length).endsWith("." + wild[2]) && !origin.includes("*");
1302
+ }
1303
+ /** The part of a widget's request that the host allows. */
1304
+ function intersectAllow(request, allow = {}) {
1305
+ const keep = (asked, max) => origins(asked).filter((o) => origins(max).some((m) => covers(m, o)));
1306
+ return {
1307
+ resourceDomains: keep(request.resourceDomains, allow.resourceDomains),
1308
+ connectDomains: keep(request.connectDomains, allow.connectDomains)
1309
+ };
1310
+ }
1311
+ /** The Content-Security-Policy of a widget's document. */
1312
+ function widgetCsp(allow = {}) {
1313
+ const res = origins(allow.resourceDomains).join(" ");
1314
+ const conn = origins(allow.connectDomains).join(" ");
1315
+ const with_ = (base) => res ? `${base} ${res}` : base;
1316
+ return [
1317
+ "default-src 'none'",
1318
+ with_("script-src 'unsafe-inline'"),
1319
+ with_("style-src 'unsafe-inline'"),
1320
+ with_("img-src data: blob:"),
1321
+ with_("font-src data:"),
1322
+ with_("media-src data: blob:"),
1323
+ `connect-src ${conn || "'none'"}`,
1324
+ "frame-src 'none'",
1325
+ "base-uri 'none'",
1326
+ "form-action 'none'"
1327
+ ].join("; ");
1328
+ }
1329
+ /** Runs inside a fragment's frame: renders the markup, reports its height, offers sendPrompt and openLink. */
1330
+ const BOOT = `(() => {
1331
+ const root = document.getElementById("widget");
1332
+ let id = 0;
1333
+ const post = (method, params, request) => parent.postMessage(request ? { jsonrpc: "2.0", id: ++id, method, params } : { jsonrpc: "2.0", method, params }, "*");
1334
+ window.sendPrompt = (text) => post("ui/message", { role: "user", content: [{ type: "text", text: String(text) }] }, true);
1335
+ window.openLink = (url) => post("ui/open-link", { url: String(url) }, true);
1336
+ let done = false;
1337
+ const runScripts = async () => {
1338
+ for (const old of [...root.querySelectorAll("script")]) {
1339
+ const s = document.createElement("script");
1340
+ for (const a of old.attributes) s.setAttribute(a.name, a.value);
1341
+ s.textContent = old.textContent;
1342
+ const loaded = s.src ? new Promise((ok) => { s.onload = s.onerror = ok; }) : null;
1343
+ old.replaceWith(s);
1344
+ if (loaded) await loaded;
1345
+ }
1346
+ };
1347
+ const theme = (ctx) => {
1348
+ const vars = (ctx && ctx.styles && ctx.styles.variables) || {};
1349
+ for (const k in vars) if (k.startsWith("--chat-")) document.documentElement.style.setProperty(k, vars[k]);
1350
+ if (ctx && ctx.theme) document.documentElement.style.colorScheme = ctx.theme;
1351
+ };
1352
+ addEventListener("message", (e) => {
1353
+ if (e.source !== parent) return;
1354
+ const m = e.data || {};
1355
+ if (m.method === "ui/notifications/host-context-changed") return theme(m.params);
1356
+ const code = m.params && m.params.arguments && m.params.arguments.widget_code;
1357
+ if (typeof code !== "string" || done) return;
1358
+ if (m.method === "ui/notifications/tool-input-partial") root.innerHTML = code;
1359
+ if (m.method === "ui/notifications/tool-input") { done = true; root.innerHTML = code; runScripts(); }
1360
+ });
1361
+ let last = -1;
1362
+ new ResizeObserver(() => {
1363
+ const h = Math.ceil(root.getBoundingClientRect().height);
1364
+ if (h !== last) { last = h; post("ui/notifications/size-changed", { height: h }); }
1365
+ }).observe(root);
1366
+ const seen = new Set();
1367
+ const report = (text) => { if (!seen.has(text) && seen.size < 10) { seen.add(text); post("notifications/message", { level: "error", logger: "widget", data: text }); } };
1368
+ addEventListener("error", (e) => report(String(e.message) + (e.lineno ? " (line " + e.lineno + ")" : "")));
1369
+ addEventListener("unhandledrejection", (e) => report("Unhandled promise rejection: " + String(e.reason && e.reason.message || e.reason)));
1370
+ post("ui/notifications/initialized", {});
1371
+ })();`;
1372
+ function fragmentDoc(csp, theme) {
1373
+ return `<!doctype html><html><head><meta charset="utf-8">
1374
+ <meta http-equiv="Content-Security-Policy" content="${csp}">
1375
+ <style>
1376
+ :root { ${Object.entries(theme.vars).map(([k, v]) => `${k}: ${v};`).join(" ")} color-scheme: ${theme.scheme}; }
1377
+ html, body { margin: 0; background: transparent; color: var(--chat-fg); font-family: var(--chat-font-sans);
1378
+ font-size: var(--chat-ui-font-size); line-height: 1.5; }
1379
+ #widget { display: flow-root; }
1380
+ *, *::before, *::after { box-sizing: border-box; }
1381
+ button, input, select, textarea { font: inherit; color: inherit; }
1382
+ </style></head><body><div id="widget"></div><script>${BOOT}<\/script></body></html>`;
1383
+ }
1384
+ /** An app's own document with the host's CSP put first in its head (a second policy can only narrow it). */
1385
+ function appDoc(csp, html) {
1386
+ const meta = `<meta http-equiv="Content-Security-Policy" content="${csp}">`;
1387
+ const head = /<head(\s[^>]*)?>/i.exec(html);
1388
+ if (head) return html.slice(0, head.index + head[0].length) + meta + html.slice(head.index + head[0].length);
1389
+ const root = /<html(\s[^>]*)?>/i.exec(html);
1390
+ if (root) return html.slice(0, root.index + root[0].length) + `<head>${meta}</head>` + html.slice(root.index + root[0].length);
1391
+ return `<!doctype html><html><head>${meta}</head><body>${html}</body></html>`;
1392
+ }
1393
+ function readTheme(el) {
1394
+ const style = getComputedStyle(el);
1395
+ return {
1396
+ vars: Object.fromEntries(THEME_VARS.map((v) => [`--chat-${v}`, style.getPropertyValue(`--chat-${v}`).trim()])),
1397
+ scheme: /light/.test(style.colorScheme) && !/dark/.test(style.colorScheme) ? "light" : "dark"
1398
+ };
1399
+ }
1400
+ /** Calls `onChange` when anything above `el` changes attributes (a theme switch) or the OS scheme flips. */
1401
+ function watchTheme(el, onChange) {
1402
+ const observer = new MutationObserver(onChange);
1403
+ for (let node = el; node; node = node.parentElement) observer.observe(node, {
1404
+ attributes: true,
1405
+ attributeFilter: [
1406
+ "class",
1407
+ "style",
1408
+ "data-chat-theme"
1409
+ ]
1410
+ });
1411
+ const media = typeof matchMedia === "function" ? matchMedia("(prefers-color-scheme: dark)") : void 0;
1412
+ media?.addEventListener?.("change", onChange);
1413
+ return () => {
1414
+ observer.disconnect();
1415
+ media?.removeEventListener?.("change", onChange);
1416
+ };
1417
+ }
1418
+ function WidgetFrame({ html, document: isDocument = false, streaming = false, title, loadingMessages, allow, csp: request, onPrompt, onOpenLink, onCallTool, onError, toolInput, toolResult, cancelled, className }) {
1419
+ const labels = useLabels();
1420
+ const frame = useRef(null);
1421
+ const host = useRef(null);
1422
+ const [ready, setReady] = useState(false);
1423
+ const [height, setHeight] = useState(0);
1424
+ const [initial, setInitial] = useState();
1425
+ const theme = useRef(void 0);
1426
+ const [tick, setTick] = useState(0);
1427
+ const effective = request ? intersectAllow(request, allow) : isDocument ? {} : allow ?? {};
1428
+ const policy = widgetCsp(effective);
1429
+ const doc = useMemo(() => initial && (isDocument ? appDoc(policy, html) : fragmentDoc(policy, initial)), [
1430
+ initial,
1431
+ policy,
1432
+ isDocument,
1433
+ isDocument && html
1434
+ ]);
1435
+ const latest = useRef({
1436
+ onPrompt,
1437
+ onOpenLink,
1438
+ onCallTool,
1439
+ onError,
1440
+ policy,
1441
+ effective
1442
+ });
1443
+ latest.current = {
1444
+ onPrompt,
1445
+ onOpenLink,
1446
+ onCallTool,
1447
+ onError,
1448
+ policy,
1449
+ effective
1450
+ };
1451
+ const send = (message) => frame.current?.contentWindow?.postMessage({
1452
+ jsonrpc: "2.0",
1453
+ ...message
1454
+ }, "*");
1455
+ const context = () => ({
1456
+ theme: theme.current?.scheme,
1457
+ styles: { variables: theme.current?.vars },
1458
+ displayMode: "inline",
1459
+ availableDisplayModes: ["inline"],
1460
+ containerDimensions: {
1461
+ maxHeight: WIDGET_MAX_HEIGHT,
1462
+ ...host.current && { width: host.current.clientWidth }
1463
+ },
1464
+ platform: "web",
1465
+ ...labels.locale && { locale: labels.locale }
1466
+ });
1467
+ useEffect(() => {
1468
+ const el = host.current;
1469
+ theme.current = readTheme(el);
1470
+ setInitial(theme.current);
1471
+ return watchTheme(el, () => {
1472
+ const next = readTheme(el);
1473
+ if (JSON.stringify(next) === JSON.stringify(theme.current)) return;
1474
+ theme.current = next;
1475
+ send({
1476
+ method: "ui/notifications/host-context-changed",
1477
+ params: {
1478
+ theme: next.scheme,
1479
+ styles: { variables: next.vars }
1480
+ }
1481
+ });
1482
+ });
1483
+ }, []);
1484
+ useEffect(() => setReady(false), [doc]);
1485
+ useEffect(() => {
1486
+ const reply = (id, result) => id !== void 0 && send({
1487
+ id,
1488
+ result
1489
+ });
1490
+ const fail = (id, code, message) => id !== void 0 && send({
1491
+ id,
1492
+ error: {
1493
+ code,
1494
+ message
1495
+ }
1496
+ });
1497
+ const onMessage = async (e) => {
1498
+ if (!frame.current || e.source !== frame.current.contentWindow) return;
1499
+ const m = e.data ?? {};
1500
+ if (typeof m !== "object" || typeof m.method !== "string") return;
1501
+ const { onPrompt, onOpenLink, onCallTool, effective } = latest.current;
1502
+ switch (m.method) {
1503
+ case "ui/initialize": return reply(m.id, {
1504
+ protocolVersion: PROTOCOL_VERSION,
1505
+ hostInfo: {
1506
+ name: "agentic-ui-kit",
1507
+ version: "1"
1508
+ },
1509
+ hostCapabilities: {
1510
+ ...onOpenLink && { openLinks: {} },
1511
+ ...onCallTool && { serverTools: {} },
1512
+ logging: {},
1513
+ sandbox: { csp: effective }
1514
+ },
1515
+ hostContext: context()
1516
+ });
1517
+ case "ui/notifications/initialized": return setReady(true);
1518
+ case "ui/notifications/size-changed": return setHeight(Math.min(Math.max(0, Number(m.params?.height) || 0), WIDGET_MAX_HEIGHT));
1519
+ case "ui/message": {
1520
+ const text = (Array.isArray(m.params?.content) ? m.params.content : []).find((c) => c?.type === "text")?.text;
1521
+ if (!onPrompt) return fail(m.id, -32e3, "Sending messages is off");
1522
+ if (typeof text !== "string" || !text.trim()) return fail(m.id, -32602, "Empty message");
1523
+ onPrompt(text.slice(0, 4e3));
1524
+ return reply(m.id, {});
1525
+ }
1526
+ case "ui/open-link": {
1527
+ const url = String(m.params?.url ?? "");
1528
+ if (!onOpenLink) return fail(m.id, -32e3, "Opening links is off");
1529
+ if (!/^https?:\/\//i.test(url)) return fail(m.id, -32602, "Only http(s) links");
1530
+ onOpenLink(url);
1531
+ return reply(m.id, {});
1532
+ }
1533
+ case "tools/call": {
1534
+ if (!onCallTool) return fail(m.id, -32e3, "Calling tools is off");
1535
+ const name = String(m.params?.name ?? "");
1536
+ const args = m.params?.arguments ?? {};
1537
+ try {
1538
+ return reply(m.id, await onCallTool(name, args));
1539
+ } catch (error) {
1540
+ return fail(m.id, -32e3, error instanceof Error ? error.message : String(error));
1541
+ }
1542
+ }
1543
+ case "ping": return reply(m.id, {});
1544
+ case "notifications/message": {
1545
+ const data = typeof m.params?.data === "string" ? m.params.data : JSON.stringify(m.params?.data);
1546
+ if (m.params?.level === "error" && data) latest.current.onError?.(data.slice(0, 1e3));
1547
+ return;
1548
+ }
1549
+ default: return fail(m.id, -32601, "Method not found");
1550
+ }
1551
+ };
1552
+ window.addEventListener("message", onMessage);
1553
+ return () => window.removeEventListener("message", onMessage);
1554
+ }, []);
1555
+ useEffect(() => {
1556
+ if (!ready || isDocument) return;
1557
+ send({
1558
+ method: streaming ? "ui/notifications/tool-input-partial" : "ui/notifications/tool-input",
1559
+ params: { arguments: { widget_code: html } }
1560
+ });
1561
+ }, [
1562
+ ready,
1563
+ isDocument,
1564
+ html,
1565
+ streaming
1566
+ ]);
1567
+ const inputKey = isDocument && toolInput !== void 0 ? JSON.stringify(toolInput) : void 0;
1568
+ useEffect(() => {
1569
+ if (!ready || inputKey === void 0) return;
1570
+ send({
1571
+ method: streaming ? "ui/notifications/tool-input-partial" : "ui/notifications/tool-input",
1572
+ params: { arguments: JSON.parse(inputKey) }
1573
+ });
1574
+ }, [
1575
+ ready,
1576
+ inputKey,
1577
+ streaming
1578
+ ]);
1579
+ useEffect(() => {
1580
+ if (ready && isDocument && toolResult !== void 0) send({
1581
+ method: "ui/notifications/tool-result",
1582
+ params: toolResult
1583
+ });
1584
+ }, [
1585
+ ready,
1586
+ isDocument,
1587
+ toolResult
1588
+ ]);
1589
+ useEffect(() => {
1590
+ if (ready && isDocument && cancelled !== void 0) send({
1591
+ method: "ui/notifications/tool-cancelled",
1592
+ params: { reason: cancelled }
1593
+ });
1594
+ }, [
1595
+ ready,
1596
+ isDocument,
1597
+ cancelled
1598
+ ]);
1599
+ const lines = loadingMessages?.length ? loadingMessages : [labels.drawingWidget];
1600
+ useEffect(() => {
1601
+ if (!streaming || lines.length < 2) return;
1602
+ const id = setInterval(() => setTick((t) => t + 1), 1800);
1603
+ return () => clearInterval(id);
1604
+ }, [streaming, lines.length]);
1605
+ return /* @__PURE__ */ jsxs("figure", {
1606
+ ref: host,
1607
+ className: className ?? "my-2",
1608
+ "aria-label": title ?? labels.widget,
1609
+ "aria-busy": streaming || void 0,
1610
+ children: [streaming && /* @__PURE__ */ jsx("div", {
1611
+ className: "mb-2 animate-pulse font-chat-sans text-(length:--chat-ui-font-size) text-chat-muted motion-reduce:animate-none",
1612
+ children: lines[tick % lines.length]
1613
+ }), doc && /* @__PURE__ */ jsx("iframe", {
1614
+ ref: frame,
1615
+ title: title ?? labels.widget,
1616
+ sandbox: "allow-scripts",
1617
+ srcDoc: doc,
1618
+ style: {
1619
+ height: height || (streaming ? 120 : 0),
1620
+ transition: "height 150ms ease-out"
1621
+ },
1622
+ className: "block w-full border-0 bg-transparent"
1623
+ })]
1624
+ });
1625
+ }
1626
+ /** Pulls the host's abilities from context into frame props. */
1627
+ function useHostProps(partId) {
1628
+ const { allow, onPrompt, onOpenLink, onCallTool, onWidgetError } = useWidgetHost();
1629
+ return {
1630
+ ...allow && { allow },
1631
+ ...onPrompt && { onPrompt },
1632
+ ...onOpenLink && { onOpenLink },
1633
+ ...onCallTool && { onCallTool },
1634
+ ...onWidgetError && { onError: (message) => onWidgetError({
1635
+ partId,
1636
+ message
1637
+ }) }
1638
+ };
1639
+ }
1640
+ /** The default look of a `widget` part: inline html, or an app UI the host reads from `resourceUri`. */
1641
+ function WidgetPartView({ part }) {
1642
+ const host = useWidgetHost();
1643
+ const props = useHostProps(part.id);
1644
+ const [resource, setResource] = useState();
1645
+ const uri = part.html === void 0 ? part.resourceUri : void 0;
1646
+ useEffect(() => {
1647
+ if (!uri || !host.readResource) return;
1648
+ let live = true;
1649
+ host.readResource(uri).then((r) => live && setResource(r), (e) => console.warn("widget:", e));
1650
+ return () => {
1651
+ live = false;
1652
+ };
1653
+ }, [uri, host.readResource]);
1654
+ if (part.html !== void 0) return /* @__PURE__ */ jsx(WidgetFrame, {
1655
+ html: part.html,
1656
+ streaming: part.status === "streaming",
1657
+ ...part.title && { title: part.title },
1658
+ ...props
1659
+ });
1660
+ if (!resource) return null;
1661
+ return /* @__PURE__ */ jsx(WidgetFrame, {
1662
+ document: true,
1663
+ html: resource.html,
1664
+ ...resource.csp && { csp: resource.csp },
1665
+ ...part.title && { title: part.title },
1666
+ ...props
1667
+ });
1668
+ }
1669
+ const str = (v) => typeof v === "string" ? v : void 0;
1670
+ /** Reads the fields of the suggested `visualizeTool` (widget_code, title, loading_messages). */
1671
+ function readVisualizeInput(input) {
1672
+ const i = input ?? {};
1673
+ const loading = Array.isArray(i.loading_messages) ? i.loading_messages.filter((l) => typeof l === "string" && l !== "") : [];
1674
+ return {
1675
+ ...str(i.widget_code) !== void 0 && { html: str(i.widget_code) },
1676
+ ...str(i.title) && { title: str(i.title) },
1677
+ ...loading.length && { loadingMessages: loading.slice(0, 4) }
1678
+ };
1679
+ }
1680
+ /**
1681
+ * A tool view that draws a call's (streaming) input in a WidgetFrame, for whatever tool your harness uses
1682
+ * to write HTML. Register it under your tool's name: `{ show_widget: widgetToolView() }`. `read` maps your
1683
+ * tool's input (possibly cut short while it streams) to the frame's fields; the default reads the suggested
1684
+ * `visualizeTool`'s fields.
1685
+ */
1686
+ function widgetToolView(read = readVisualizeInput) {
1687
+ function WidgetToolBlock({ part }) {
1688
+ const props = useHostProps(part.id);
1689
+ const { html = "", title, loadingMessages } = read(part.input ?? part.partialInput);
1690
+ if (part.status === "error" && !html) return null;
1691
+ return /* @__PURE__ */ jsx(WidgetFrame, {
1692
+ html,
1693
+ streaming: part.status === "streaming" || part.status === "input",
1694
+ ...title && { title },
1695
+ ...loadingMessages?.length && { loadingMessages },
1696
+ ...props
1697
+ });
1698
+ }
1699
+ return { Block: WidgetToolBlock };
1700
+ }
1701
+ /** The default `onOpenLink` of ChatView and ChatTranscript: a new tab with no opener. */
1702
+ function openInNewTab(url) {
1703
+ window.open(url, "_blank", "noopener,noreferrer");
1704
+ }
1705
+ // src/components/ui-tree.tsx
1706
+ const RegistryContext = createContext(void 0);
1707
+ /**
1708
+ * The components the model may use: in ```ui blocks of its replies (see `uiMarkupGuide` in
1709
+ * agentic-ui-kit/core) and in tool calls drawn with `uiTreeToolView`. `persist` keeps each widget's state.
1710
+ */
1711
+ function UiComponentsProvider({ components, persist, markup = "ui", children }) {
1712
+ const value = useMemo(() => ({
1713
+ components: new Map(components.map((c) => [c.name, c])),
1714
+ persist,
1715
+ markup
1716
+ }), [
1717
+ components,
1718
+ persist,
1719
+ markup
1720
+ ]);
1721
+ return /* @__PURE__ */ jsx(RegistryContext.Provider, {
1722
+ value,
1723
+ children
1724
+ });
1725
+ }
1726
+ const TreeContext = createContext({
1727
+ state: {},
1728
+ run: () => {},
1729
+ streaming: false
1730
+ });
1731
+ /** Inside a registered component: runs an action from its props, e.g. `run(props.onPress)`. Bad actions do nothing. */
1732
+ function useUiAction() {
1733
+ const { run } = useContext(TreeContext);
1734
+ return useCallback((action) => matchesSchema(uiActionSchema, action) && run(action), [run]);
1735
+ }
1736
+ /** Inside a registered component: the widget's state and a setter, for inputs that write to it. */
1737
+ function useUiState() {
1738
+ const { state, run } = useContext(TreeContext);
1739
+ return [state, useCallback((key, value) => run({
1740
+ action: "set",
1741
+ key,
1742
+ value
1743
+ }), [run])];
1744
+ }
1745
+ /** Replaces `{ "$state": key }` anywhere in the props with the state's value. */
1746
+ function resolve(value, state) {
1747
+ if (isStateRef(value)) return state[value.$state];
1748
+ if (Array.isArray(value)) return value.map((v) => resolve(v, state));
1749
+ if (typeof value === "object" && value !== null) {
1750
+ const out = {};
1751
+ for (const [k, v] of Object.entries(value)) {
1752
+ const r = resolve(v, state);
1753
+ if (r !== void 0) out[k] = r;
1754
+ }
1755
+ return out;
1756
+ }
1757
+ return value;
1758
+ }
1759
+ /** A component that throws renders nothing instead of taking the message down. */
1760
+ var Contain = class extends Component {
1761
+ state = { failed: false };
1762
+ static getDerivedStateFromError() {
1763
+ return { failed: true };
1764
+ }
1765
+ componentDidCatch(error) {
1766
+ console.warn("render_ui:", error);
1767
+ }
1768
+ render() {
1769
+ return this.state.failed ? null : this.props.children;
1770
+ }
1771
+ };
1772
+ function Node({ node, registry }) {
1773
+ const { state } = useContext(TreeContext);
1774
+ if (Array.isArray(node)) return node.map((child, i) => /* @__PURE__ */ jsx(Node, {
1775
+ node: child,
1776
+ registry
1777
+ }, i));
1778
+ if (typeof node !== "object" || node === null) return null;
1779
+ const { component, props = {}, children } = node;
1780
+ const spec = typeof component === "string" ? registry.components.get(component) : void 0;
1781
+ if (!spec || typeof props !== "object") return null;
1782
+ const resolved = resolve(props, state);
1783
+ if (!matchesSchema(spec.props ?? { type: "object" }, resolved)) return null;
1784
+ const Render = spec.render;
1785
+ const kids = spec.children && Array.isArray(children) ? children.map((child, i) => /* @__PURE__ */ jsx(Node, {
1786
+ node: child,
1787
+ registry
1788
+ }, i)) : void 0;
1789
+ return /* @__PURE__ */ jsx(Contain, { children: /* @__PURE__ */ jsx(Render, {
1790
+ ...resolved,
1791
+ children: kids
1792
+ }) });
1793
+ }
1794
+ /** Draws a `render_ui` tree with the registered components. Unknown components and bad props render nothing. */
1795
+ function UiTree({ id, root, state: initial, streaming = false }) {
1796
+ const registry = useContext(RegistryContext);
1797
+ const host = useWidgetHost();
1798
+ const [local, setLocal] = useState(() => registry?.persist?.load(id) ?? {});
1799
+ const changed = useRef(false);
1800
+ const latest = useRef(host);
1801
+ latest.current = host;
1802
+ useEffect(() => {
1803
+ if (changed.current) registry?.persist?.save(id, local);
1804
+ }, [
1805
+ id,
1806
+ local,
1807
+ registry
1808
+ ]);
1809
+ const run = useCallback((action) => {
1810
+ switch (action.action) {
1811
+ case "set":
1812
+ changed.current = true;
1813
+ return setLocal((s) => ({
1814
+ ...s,
1815
+ [action.key]: action.value
1816
+ }));
1817
+ case "send": return latest.current.onPrompt?.(action.text);
1818
+ case "open": return /^https?:\/\//i.test(action.url) && latest.current.onOpenLink?.(action.url);
1819
+ case "copy":
1820
+ navigator.clipboard?.writeText(action.text).catch(() => {});
1821
+ return;
1822
+ }
1823
+ }, []);
1824
+ const state = useMemo(() => ({
1825
+ ...typeof initial === "object" && initial ? initial : {},
1826
+ ...local
1827
+ }), [initial, local]);
1828
+ const context = useMemo(() => ({
1829
+ state,
1830
+ run,
1831
+ streaming
1832
+ }), [
1833
+ state,
1834
+ run,
1835
+ streaming
1836
+ ]);
1837
+ if (!registry) return null;
1838
+ return /* @__PURE__ */ jsx(TreeContext.Provider, {
1839
+ value: context,
1840
+ children: /* @__PURE__ */ jsx("div", {
1841
+ className: "chat-ui-tree my-2",
1842
+ "aria-busy": streaming || void 0,
1843
+ children: /* @__PURE__ */ jsx(Node, {
1844
+ node: root,
1845
+ registry
1846
+ })
1847
+ })
1848
+ });
1849
+ }
1850
+ /** The fence language drawn as UI here, or undefined outside a `UiComponentsProvider`. */
1851
+ function useUiMarkupLanguage() {
1852
+ const registry = useContext(RegistryContext);
1853
+ return registry && registry.markup !== false ? registry.markup : void 0;
1854
+ }
1855
+ /** A ```ui block from a reply, drawn with the registered components. Safe on a block that is still streaming. */
1856
+ function UiMarkupBlock({ id, source, streaming = false }) {
1857
+ const { nodes, state } = useMemo(() => parseUiMarkup(source), [source]);
1858
+ return /* @__PURE__ */ jsx(UiTree, {
1859
+ id,
1860
+ root: nodes,
1861
+ state,
1862
+ streaming
1863
+ });
1864
+ }
1865
+ /**
1866
+ * A tool view that draws a call's (streaming) JSX markup with your registered components, for whatever tool
1867
+ * your harness uses; the suggested one is core's `showUiTool`. Register it under your tool's name:
1868
+ * `{ show_ui: uiMarkupToolView() }`. `read` maps your tool's input to the markup; the default reads `markup`.
1869
+ */
1870
+ function uiMarkupToolView(read = (input) => {
1871
+ const markup = input?.markup;
1872
+ return typeof markup === "string" ? markup : void 0;
1873
+ }) {
1874
+ function UiMarkupToolBlock({ part }) {
1875
+ const markup = read(part.input ?? part.partialInput);
1876
+ if (!markup) return null;
1877
+ return /* @__PURE__ */ jsx(UiMarkupBlock, {
1878
+ id: part.id,
1879
+ source: markup,
1880
+ streaming: part.status === "streaming" || part.status === "input"
1881
+ });
1882
+ }
1883
+ return { Block: UiMarkupToolBlock };
1884
+ }
1885
+ /** Reads `{ root, state }`. Models sometimes send `root` or `state` as a JSON string; those are parsed too. */
1886
+ function readTreeInput(input) {
1887
+ const i = input ?? {};
1888
+ const unwrap = (v) => typeof v === "string" ? salvageJson(v).value : v;
1889
+ const root = unwrap(i.root);
1890
+ const state = unwrap(i.state);
1891
+ return {
1892
+ ...root !== void 0 && { root },
1893
+ ...typeof state === "object" && state !== null && { state }
1894
+ };
1895
+ }
1896
+ /**
1897
+ * A tool view that draws a call's (streaming) input as a tree of your registered components, for whatever
1898
+ * tool your harness uses. Register it under your tool's name: `{ render_ui: uiTreeToolView() }`. `read` maps
1899
+ * your tool's input to the tree and its starting state; the default reads `{ root, state }`, the shape of
1900
+ * the suggested `renderUiTool`.
1901
+ */
1902
+ function uiTreeToolView(read = readTreeInput) {
1903
+ function UiTreeToolBlock({ part }) {
1904
+ const { root, state } = read(part.input ?? part.partialInput);
1905
+ if (!root) return null;
1906
+ const ok = typeof state === "object" && state !== null;
1907
+ return /* @__PURE__ */ jsx(UiTree, {
1908
+ id: part.id,
1909
+ root,
1910
+ ...ok && { state },
1911
+ streaming: part.status === "streaming" || part.status === "input"
1912
+ });
1913
+ }
1914
+ return { Block: UiTreeToolBlock };
1915
+ }
1916
+ /**
1917
+ * The escape hatch: raw HTML or SVG in a sandboxed WidgetFrame, as a node of the tree. Register it only if
1918
+ * the model may write HTML; it gets what the chat's `widgets` host allows.
1919
+ */
1920
+ const htmlComponent = {
1921
+ name: "Html",
1922
+ description: "Raw HTML or <svg> in a sandboxed frame, only for what the other components cannot show. Follow the visual design guide.",
1923
+ props: {
1924
+ type: "object",
1925
+ properties: {
1926
+ html: { type: "string" },
1927
+ title: { type: "string" }
1928
+ },
1929
+ required: ["html"]
1930
+ },
1931
+ render: function Html({ html, title }) {
1932
+ const { streaming } = useContext(TreeContext);
1933
+ const { allow, onPrompt, onOpenLink } = useWidgetHost();
1934
+ return /* @__PURE__ */ jsx(WidgetFrame, {
1935
+ html,
1936
+ streaming,
1937
+ ...title && { title },
1938
+ ...allow && { allow },
1939
+ ...onPrompt && { onPrompt },
1940
+ ...onOpenLink && { onOpenLink }
1941
+ });
1942
+ }
1943
+ };
1015
1944
  // src/components/word-fade.ts
1016
1945
  const OPAQUE_TAGS = /* @__PURE__ */ new Set([
1017
1946
  "code",
@@ -1078,45 +2007,96 @@ function textOf(node) {
1078
2007
  if (node.type === "text") return node.value ?? "";
1079
2008
  return (node.children ?? []).map(textOf).join("");
1080
2009
  }
2010
+ const BlockContext = createContext(void 0);
2011
+ function Code({ node, className, children }) {
2012
+ const hast = node;
2013
+ const language = /language-([\w+#-]+)/.exec(className ?? "")?.[1];
2014
+ const ui = useUiMarkupLanguage();
2015
+ const block = useContext(BlockContext);
2016
+ const multiline = hast?.position && hast.position.start.line !== hast.position.end.line;
2017
+ if (!language && !multiline) return /* @__PURE__ */ jsx("code", {
2018
+ "data-chat-inline-code": true,
2019
+ children
2020
+ });
2021
+ const code = textOf(hast).replace(/\n$/, "");
2022
+ if (block && ui && language === ui) return /* @__PURE__ */ jsx(UiMarkupBlock, {
2023
+ id: `${block.id}:${hast?.position?.start.line ?? 0}`,
2024
+ source: code,
2025
+ streaming: block.streaming
2026
+ });
2027
+ return /* @__PURE__ */ jsx(CodeBlock, {
2028
+ variant: "boxed",
2029
+ code,
2030
+ ...language && { language }
2031
+ });
2032
+ }
1081
2033
  const components = {
1082
2034
  pre: ({ children }) => /* @__PURE__ */ jsx(Fragment, { children }),
1083
- code: ({ node, className, children }) => {
1084
- const hast = node;
1085
- const language = /language-([\w+#-]+)/.exec(className ?? "")?.[1];
1086
- const multiline = hast?.position && hast.position.start.line !== hast.position.end.line;
1087
- if (!language && !multiline) return /* @__PURE__ */ jsx("code", {
1088
- "data-chat-inline-code": true,
1089
- children
1090
- });
1091
- return /* @__PURE__ */ jsx(CodeBlock, {
1092
- variant: "boxed",
1093
- code: textOf(hast).replace(/\n$/, ""),
1094
- ...language && { language }
1095
- });
1096
- }
2035
+ code: ({ node, className, children }) => /* @__PURE__ */ jsx(Code, {
2036
+ node,
2037
+ className,
2038
+ children
2039
+ })
1097
2040
  };
2041
+ const LinksContext = createContext(void 0);
2042
+ function MarkdownLinksProvider({ renderLink, children }) {
2043
+ return /* @__PURE__ */ jsx(LinksContext.Provider, {
2044
+ value: renderLink,
2045
+ children
2046
+ });
2047
+ }
1098
2048
  /** Message markdown: GFM, KaTeX math ($…$ and $$…$$), Shiki code blocks, smooth fade-in while streaming. */
1099
- function Markdown({ children, streaming = false, className }) {
2049
+ function Markdown({ children, streaming = false, className, partId }) {
1100
2050
  const text = useSmoothText(children, streaming);
1101
2051
  const animating = streaming || text.length < children.length;
1102
2052
  const [streamed, setStreamed] = useState(streaming);
1103
2053
  if (streaming && !streamed) setStreamed(true);
1104
2054
  return /* @__PURE__ */ jsx("div", {
1105
2055
  className: clsx("chat-markdown", streamed && "chat-fade", className),
1106
- children: /* @__PURE__ */ jsx(MarkdownBody, {
1107
- text,
1108
- animating,
1109
- fade: streamed
2056
+ children: /* @__PURE__ */ jsx(BlockScope, {
2057
+ id: partId,
2058
+ streaming: animating,
2059
+ children: /* @__PURE__ */ jsx(MarkdownBody, {
2060
+ text,
2061
+ animating,
2062
+ fade: streamed
2063
+ })
1110
2064
  })
1111
2065
  });
1112
2066
  }
2067
+ function BlockScope({ id, streaming, children }) {
2068
+ const value = useMemo(() => id ? {
2069
+ id,
2070
+ streaming
2071
+ } : void 0, [id, streaming]);
2072
+ return /* @__PURE__ */ jsx(BlockContext.Provider, {
2073
+ value,
2074
+ children
2075
+ });
2076
+ }
1113
2077
  const MarkdownBody = memo(function MarkdownBody({ text, animating, fade }) {
2078
+ const renderLink = useContext(LinksContext);
2079
+ const withLinks = useMemo(() => renderLink ? {
2080
+ ...components,
2081
+ a: ({ href, children }) => {
2082
+ const custom = href ? renderLink({
2083
+ href,
2084
+ children
2085
+ }) : void 0;
2086
+ return custom !== void 0 ? /* @__PURE__ */ jsx(Fragment, { children: custom }) : /* @__PURE__ */ jsx("a", {
2087
+ href,
2088
+ target: "_blank",
2089
+ rel: "noopener noreferrer",
2090
+ children
2091
+ });
2092
+ }
2093
+ } : components, [renderLink]);
1114
2094
  return /* @__PURE__ */ jsx(Streamdown, {
1115
2095
  prefix: PREFIX,
1116
2096
  mode: animating || fade ? "streaming" : "static",
1117
2097
  ...fade && { rehypePlugins: fadePlugins },
1118
2098
  plugins: { math },
1119
- components,
2099
+ components: withLinks,
1120
2100
  controls: false,
1121
2101
  lineNumbers: false,
1122
2102
  linkSafety: { enabled: false },
@@ -1156,32 +2136,31 @@ const useChatTool = useAgentTool;
1156
2136
  /** Streaming or running: the model or a tool is busy with it. */
1157
2137
  const isWorking = (part) => part.status !== "done" && part.status !== "error" && part.status !== "awaiting";
1158
2138
  /** Row label for any activity part. */
1159
- function activityLabel(part, view) {
2139
+ function activityLabel(part, labels, view) {
1160
2140
  if (part.type === "thinking") {
1161
2141
  if (part.title) return part.title;
1162
- if (part.status !== "done") return "Thinking…";
1163
- return part.durationMs ? `Thought for ${formatDuration(part.durationMs)}` : "Thought";
2142
+ if (part.status !== "done") return labels.thinking;
2143
+ return part.durationMs ? labels.thoughtFor(formatDuration(part.durationMs, labels)) : labels.thought;
1164
2144
  }
1165
- return view?.label?.(part) ?? part.title ?? part.name;
2145
+ return view?.label?.(part, labels) ?? part.title ?? part.name;
1166
2146
  }
1167
2147
  /**
1168
2148
  * Heading of an activity group. While live it follows the latest step. Once done it lists what the tools
1169
2149
  * did, in order ("Ran a command, searched the web"); a group of only thinking shows the first title.
1170
2150
  */
1171
- function activityHeading(parts, views) {
2151
+ function activityHeading(parts, views, labels) {
1172
2152
  const waiting = parts.find((p) => p.type === "tool_call" && p.status === "awaiting");
1173
- if (waiting) return /* @__PURE__ */ jsxs(Fragment, { children: [activityLabel(waiting, views[waiting.name]), " · waiting for you"] });
2153
+ if (waiting) return /* @__PURE__ */ jsxs(Fragment, { children: [activityLabel(waiting, labels, views[waiting.name]), labels.waitingForYou] });
1174
2154
  if (parts.some(isWorking)) {
1175
2155
  const last = parts.at(-1);
1176
- return activityLabel(last, last.type === "tool_call" ? views[last.name] : void 0);
2156
+ return activityLabel(last, labels, last.type === "tool_call" ? views[last.name] : void 0);
1177
2157
  }
1178
2158
  const counts = /* @__PURE__ */ new Map();
1179
2159
  for (const part of parts) if (part.type === "tool_call") counts.set(part.name, (counts.get(part.name) ?? 0) + 1);
1180
- if (counts.size === 0) return parts[0] ? activityLabel(parts[0]) : "";
1181
- const text = [...counts].map(([name, count]) => views[name]?.summary?.(count) ?? `used ${name}`).join(", ");
1182
- return text.charAt(0).toUpperCase() + text.slice(1);
2160
+ if (counts.size === 0) return parts[0] ? activityLabel(parts[0], labels) : "";
2161
+ const phrases = [...counts].map(([name, count]) => views[name]?.summary?.(count, labels) ?? labels.usedTool(name));
2162
+ return labels.joinSummaries(phrases);
1183
2163
  }
1184
- const plural = (count, one, many) => count === 1 ? one : many(count);
1185
2164
  function recordOf(value) {
1186
2165
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1187
2166
  }
@@ -1190,10 +2169,11 @@ function stringOf(value) {
1190
2169
  }
1191
2170
  /** A shell command. Input `{ command, description? }`; result a string or `{ stdout, stderr, exitCode }`. */
1192
2171
  const commandTool = {
1193
- label: () => "Ran a command",
2172
+ label: (_, labels) => labels.ranCommandLabel,
1194
2173
  hint: (part) => stringOf(recordOf(part.input).description),
1195
- summary: (n) => plural(n, "ran a command", (n) => `ran ${n} commands`),
2174
+ summary: (n, labels) => labels.ranCommands(n),
1196
2175
  Detail: function CommandDetail({ part }) {
2176
+ const labels = useLabels();
1197
2177
  const command = stringOf(recordOf(part.input).command) ?? part.inputJson;
1198
2178
  const result = recordOf(part.result);
1199
2179
  const output = stringOf(part.result) ?? [stringOf(result.stdout), stringOf(result.stderr)].filter(Boolean).join("\n");
@@ -1205,31 +2185,37 @@ const commandTool = {
1205
2185
  code: command
1206
2186
  }),
1207
2187
  (output || part.status === "done") && /* @__PURE__ */ jsx(CodeBlock, {
1208
- label: "Output",
1209
- code: output || "(no output)"
2188
+ label: labels.output,
2189
+ code: output || labels.noOutput
1210
2190
  }),
1211
2191
  part.error && /* @__PURE__ */ jsx("p", {
1212
- className: "text-sm text-chat-danger",
2192
+ className: "text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
1213
2193
  children: part.error
1214
2194
  })
1215
2195
  ]
1216
2196
  });
1217
2197
  }
1218
2198
  };
1219
- /** A web search. Input `{ query }`; result `{ results: SearchResult[] }` or `SearchResult[]`. */
2199
+ /**
2200
+ * A web search. Input `{ query }`; results (`{ results: SearchResult[] }` or `SearchResult[]`) from the
2201
+ * part's display `data`, else its `result`.
2202
+ */
1220
2203
  const webSearchTool = {
1221
- label: () => "Searched the web",
2204
+ label: (_, labels) => labels.searchedWebLabel,
1222
2205
  subtitle: (part) => stringOf(recordOf(part.input).query) ?? part.subtitle,
1223
- summary: (n) => plural(n, "searched the web", (n) => `searched the web ${n} times`),
2206
+ summary: (n, labels) => labels.searchedWeb(n),
1224
2207
  Detail: function WebSearchDetail({ part }) {
1225
- const results = Array.isArray(part.result) ? part.result : recordOf(part.result).results;
2208
+ const labels = useLabels();
2209
+ const icons = useIcons();
2210
+ const shown = part.data ?? part.result;
2211
+ const results = Array.isArray(shown) ? shown : recordOf(shown).results;
1226
2212
  if (part.error) return /* @__PURE__ */ jsx("p", {
1227
- className: "text-sm text-chat-danger",
2213
+ className: "text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
1228
2214
  children: part.error
1229
2215
  });
1230
2216
  if (!results?.length) return /* @__PURE__ */ jsx("p", {
1231
- className: "text-sm text-chat-muted",
1232
- children: "No results"
2217
+ className: "text-(length:--chat-ui-font-size) leading-5 text-chat-muted",
2218
+ children: labels.noResults
1233
2219
  });
1234
2220
  return /* @__PURE__ */ jsx("ul", {
1235
2221
  className: "-mx-1",
@@ -1237,22 +2223,22 @@ const webSearchTool = {
1237
2223
  href: result.url,
1238
2224
  target: "_blank",
1239
2225
  rel: "noreferrer",
1240
- className: "flex items-center gap-2.5 rounded-chat-sm px-1 py-2 text-[0.9375rem] text-chat-muted outline-none hover:text-chat-fg focus-visible:ring-2 focus-visible:ring-chat-focus",
2226
+ className: "flex items-center gap-2.5 rounded-chat-sm px-1 py-2 text-(length:--chat-activity-font-size) text-chat-muted outline-none hover:text-chat-fg focus-visible:ring-2 focus-visible:ring-chat-focus",
1241
2227
  children: [
1242
2228
  result.favicon ? /* @__PURE__ */ jsx("img", {
1243
2229
  src: result.favicon,
1244
2230
  alt: "",
1245
2231
  className: "size-4 shrink-0 rounded-sm"
1246
- }) : /* @__PURE__ */ jsx(Globe, {
1247
- "aria-hidden": true,
1248
- className: "size-4 shrink-0"
2232
+ }) : /* @__PURE__ */ jsx(IconSlot, {
2233
+ className: "[&_svg]:size-4",
2234
+ children: icons.web
1249
2235
  }),
1250
2236
  /* @__PURE__ */ jsx("span", {
1251
2237
  className: "min-w-0 flex-1 truncate",
1252
2238
  children: result.title
1253
2239
  }),
1254
2240
  /* @__PURE__ */ jsx("span", {
1255
- className: "shrink-0 text-sm",
2241
+ className: "shrink-0 text-(length:--chat-ui-font-size) leading-5",
1256
2242
  children: hostOf(result.url)
1257
2243
  })
1258
2244
  ]
@@ -1260,18 +2246,44 @@ const webSearchTool = {
1260
2246
  });
1261
2247
  }
1262
2248
  };
2249
+ /** Reads a web page. Input `{ url }`; the row shows the page's host, the detail links to it. */
2250
+ const webFetchTool = {
2251
+ label: (_, labels) => labels.readPageLabel,
2252
+ subtitle: (part) => {
2253
+ const url = stringOf(recordOf(part.input).url);
2254
+ return url ? hostOf(url) : part.subtitle;
2255
+ },
2256
+ summary: (n, labels) => labels.readPages(n),
2257
+ Detail: function WebFetchDetail({ part }) {
2258
+ const url = stringOf(recordOf(part.input).url);
2259
+ const title = stringOf(recordOf(part.data).title);
2260
+ if (part.error) return /* @__PURE__ */ jsx("p", {
2261
+ className: "text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
2262
+ children: part.error
2263
+ });
2264
+ if (!url) return null;
2265
+ return /* @__PURE__ */ jsx("a", {
2266
+ href: url,
2267
+ target: "_blank",
2268
+ rel: "noopener noreferrer",
2269
+ className: "block truncate text-(length:--chat-ui-font-size) leading-5 text-chat-fg hover:underline",
2270
+ children: title ?? url
2271
+ });
2272
+ }
2273
+ };
1263
2274
  /** Memory lookups, e.g. Anthropic's memory tool. Input `{ path }`. */
1264
2275
  const memoryTool = {
1265
- label: () => "Recalled memory",
2276
+ label: (_, labels) => labels.recalledMemoryLabel,
1266
2277
  subtitle: (part) => {
1267
2278
  return stringOf(recordOf(part.input).path)?.split("/").at(-1) ?? part.subtitle;
1268
2279
  },
1269
- summary: () => "recalled memory"
2280
+ summary: (n, labels) => labels.recalledMemory(n)
1270
2281
  };
1271
2282
  const defaultToolViews = {
1272
2283
  bash: commandTool,
1273
2284
  shell: commandTool,
1274
2285
  web_search: webSearchTool,
2286
+ web_fetch: webFetchTool,
1275
2287
  memory: memoryTool,
1276
2288
  memory_view: memoryTool
1277
2289
  };
@@ -1283,9 +2295,8 @@ function hostOf(url) {
1283
2295
  }
1284
2296
  }
1285
2297
  /** "9s", "1m 5s". */
1286
- function formatDuration(ms) {
1287
- const seconds = Math.max(1, Math.round(ms / 1e3));
1288
- return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
2298
+ function formatDuration(ms, labels = defaultLabels) {
2299
+ return labels.duration(Math.max(1, Math.round(ms / 1e3)));
1289
2300
  }
1290
2301
  // src/components/token-counter.tsx
1291
2302
  /** Compact token count: 526, 1.4k, 33k, 76.8k, 1M. */
@@ -1330,25 +2341,23 @@ function useCountUp(target) {
1330
2341
  }
1331
2342
  /** A token count that counts up as `tokens` grows and holds still when it stops growing. */
1332
2343
  function TokenCounter({ tokens, variant = "standalone", className }) {
2344
+ const icons = useIcons();
1333
2345
  const shown = useCountUp(tokens);
2346
+ const labels = useLabels();
1334
2347
  if (variant === "inline") return /* @__PURE__ */ jsxs("span", {
1335
2348
  className: clsx("text-chat-subtle tabular-nums", className),
1336
2349
  children: [
1337
2350
  "(",
1338
- formatTokens(shown),
1339
- " tokens)"
2351
+ labels.tokenCount(formatTokens(shown)),
2352
+ ")"
1340
2353
  ]
1341
2354
  });
1342
2355
  return /* @__PURE__ */ jsxs("span", {
1343
- className: clsx("inline-flex items-center gap-1 font-chat-sans text-xs text-chat-subtle tabular-nums", className),
1344
- children: [
1345
- /* @__PURE__ */ jsx(ArrowDown, {
1346
- "aria-hidden": true,
1347
- className: "size-3"
1348
- }),
1349
- formatTokens(shown),
1350
- " tokens"
1351
- ]
2356
+ className: clsx("inline-flex items-center gap-1 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle tabular-nums", className),
2357
+ children: [/* @__PURE__ */ jsx(IconSlot, {
2358
+ className: "[&_svg]:size-3",
2359
+ children: icons.tokens
2360
+ }), labels.tokenCount(formatTokens(shown))]
1352
2361
  });
1353
2362
  }
1354
2363
  // src/components/activity-group.tsx
@@ -1359,6 +2368,7 @@ function TokenCounter({ tokens, variant = "standalone", className }) {
1359
2368
  */
1360
2369
  function ActivityGroup({ parts, defaultExpanded = false, className }) {
1361
2370
  const views = useToolViews();
2371
+ const labels = useLabels();
1362
2372
  const [expanded, setExpanded] = useState(defaultExpanded);
1363
2373
  const live = parts.some(isWorking);
1364
2374
  const tokens = activityTokens(parts);
@@ -1380,9 +2390,9 @@ function ActivityGroup({ parts, defaultExpanded = false, className }) {
1380
2390
  onExpandedChange: setExpanded,
1381
2391
  summary: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Live, {
1382
2392
  live,
1383
- children: activityHeading(parts, views)
2393
+ children: activityHeading(parts, views, labels)
1384
2394
  }), after] }),
1385
- summaryClassName: "text-[0.9375rem]",
2395
+ summaryClassName: "text-(length:--chat-activity-font-size)",
1386
2396
  panelClassName: "pt-3",
1387
2397
  children: /* @__PURE__ */ jsx("div", {
1388
2398
  className: "divide-y divide-chat-divider rounded-chat-md border border-chat-divider",
@@ -1419,7 +2429,7 @@ function SingleStep({ part, live, after, expanded, onExpandedChange }) {
1419
2429
  live,
1420
2430
  children: summary
1421
2431
  }), after] }),
1422
- summaryClassName: "text-[0.9375rem]",
2432
+ summaryClassName: "text-(length:--chat-activity-font-size)",
1423
2433
  panelClassName: "pt-3",
1424
2434
  children: /* @__PURE__ */ jsx("div", {
1425
2435
  className: "rounded-chat-md border border-chat-divider px-3 py-2",
@@ -1432,7 +2442,7 @@ function Step({ part }) {
1432
2442
  const live = isWorking(part);
1433
2443
  return /* @__PURE__ */ jsx(Disclosure, {
1434
2444
  className: "px-3 py-2",
1435
- summaryClassName: "text-[0.9375rem]",
2445
+ summaryClassName: "text-(length:--chat-activity-font-size)",
1436
2446
  panelClassName: "pt-2 pb-1",
1437
2447
  summary: /* @__PURE__ */ jsx(Live, {
1438
2448
  live,
@@ -1443,12 +2453,13 @@ function Step({ part }) {
1443
2453
  }
1444
2454
  /** The label line and expanded body of one step. Tool calls go through their registered view. */
1445
2455
  function useStepContent(part) {
2456
+ const labels = useLabels();
1446
2457
  const view = useToolView(part.type === "tool_call" ? part.name : "");
1447
2458
  if (part.type === "thinking") return {
1448
- summary: activityLabel(part),
2459
+ summary: activityLabel(part, labels),
1449
2460
  detail: part.visibility === "hidden" || !part.text ? /* @__PURE__ */ jsx("p", {
1450
- className: "text-sm text-chat-subtle",
1451
- children: "The model's reasoning is not shown."
2461
+ className: "text-(length:--chat-ui-font-size) leading-5 text-chat-subtle",
2462
+ children: labels.reasoningHidden
1452
2463
  }) : /* @__PURE__ */ jsx("div", {
1453
2464
  className: "max-h-96 overflow-y-auto [scrollbar-width:thin]",
1454
2465
  children: /* @__PURE__ */ jsx(Markdown, {
@@ -1463,7 +2474,7 @@ function useStepContent(part) {
1463
2474
  return {
1464
2475
  summary: /* @__PURE__ */ jsxs("span", {
1465
2476
  title: view.hint?.(part),
1466
- children: [activityLabel(part, view), subtitle && /* @__PURE__ */ jsx("span", {
2477
+ children: [activityLabel(part, labels, view), subtitle && /* @__PURE__ */ jsx("span", {
1467
2478
  className: "ml-1.5 text-chat-fg",
1468
2479
  children: subtitle
1469
2480
  })]
@@ -1473,16 +2484,16 @@ function useStepContent(part) {
1473
2484
  }
1474
2485
  function DefaultDetail({ part }) {
1475
2486
  if (part.error) return /* @__PURE__ */ jsx("p", {
1476
- className: "text-sm text-chat-danger",
2487
+ className: "text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
1477
2488
  children: part.error
1478
2489
  });
1479
2490
  const body = part.result ?? part.input ?? part.inputJson;
1480
2491
  if (typeof body === "string") return /* @__PURE__ */ jsx("p", {
1481
- className: "text-[0.9375rem] leading-relaxed whitespace-pre-wrap text-chat-muted",
2492
+ className: "text-(length:--chat-activity-font-size) leading-relaxed whitespace-pre-wrap text-chat-muted",
1482
2493
  children: body
1483
2494
  });
1484
2495
  return /* @__PURE__ */ jsx("pre", {
1485
- className: "max-h-56 overflow-auto font-chat-mono text-[0.8125rem] leading-relaxed text-chat-muted",
2496
+ className: "max-h-56 overflow-auto font-chat-mono text-(length:--chat-code-font-size) leading-relaxed text-chat-muted",
1486
2497
  children: JSON.stringify(body, null, 2)
1487
2498
  });
1488
2499
  }
@@ -1493,6 +2504,9 @@ const draftId = () => `att_${Date.now().toString(36)}_${(counter++).toString(36)
1493
2504
  function useAttachments({ upload } = {}) {
1494
2505
  const [attachments, setAttachments] = useState([]);
1495
2506
  const uploads = useRef(/* @__PURE__ */ new Map());
2507
+ const labels = useLabels();
2508
+ const labelsRef = useRef(labels);
2509
+ labelsRef.current = labels;
1496
2510
  const uploadRef = useRef(upload);
1497
2511
  uploadRef.current = upload;
1498
2512
  const patch = useCallback((id, fields) => {
@@ -1526,7 +2540,7 @@ function useAttachments({ upload } = {}) {
1526
2540
  })).catch((error) => {
1527
2541
  if (!controller.signal.aborted) patch(id, {
1528
2542
  status: "error",
1529
- error: error instanceof Error ? error.message : "Upload failed"
2543
+ error: error instanceof Error ? error.message : labelsRef.current.uploadFailed
1530
2544
  });
1531
2545
  }).finally(() => uploads.current.delete(id));
1532
2546
  }
@@ -1535,7 +2549,7 @@ function useAttachments({ upload } = {}) {
1535
2549
  setAttachments((list) => [...list, {
1536
2550
  id: draftId(),
1537
2551
  kind: "text",
1538
- name: "Pasted text",
2552
+ name: labelsRef.current.pastedText,
1539
2553
  mime: "text/plain",
1540
2554
  size: new Blob([text]).size,
1541
2555
  text,
@@ -1581,13 +2595,15 @@ function useAttachments({ upload } = {}) {
1581
2595
  const extension = (name) => /\.([a-z0-9]{1,5})$/i.exec(name)?.[1]?.toUpperCase();
1582
2596
  /** A square card: pasted text preview with a PASTED badge, an image thumbnail, or a file with its type. */
1583
2597
  function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", className }) {
2598
+ const icons = useIcons();
1584
2599
  const image = a.mime.startsWith("image/") && a.url;
1585
2600
  const pasted = a.text !== void 0;
2601
+ const labels = useLabels();
1586
2602
  const uploading = a.status === "uploading";
1587
2603
  const failed = a.status === "error";
1588
2604
  return /* @__PURE__ */ jsxs("div", {
1589
2605
  className: clsx("group/card relative shrink-0 rounded-chat-md border bg-chat-bg font-chat-sans", failed ? "border-chat-danger" : "border-chat-border", size === "md" ? "size-30" : "size-24", className),
1590
- title: failed ? a.error ?? "Upload failed" : a.name,
2606
+ title: failed ? a.error ?? labels.uploadFailed : a.name,
1591
2607
  children: [
1592
2608
  /* @__PURE__ */ jsx("div", {
1593
2609
  className: clsx("absolute inset-0 overflow-hidden rounded-[inherit]", uploading && "chat-shimmer-surface"),
@@ -1600,11 +2616,11 @@ function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", classNam
1600
2616
  children: a.text
1601
2617
  }) : /* @__PURE__ */ jsxs("div", {
1602
2618
  className: "flex h-full flex-col gap-1.5 p-2.5",
1603
- children: [/* @__PURE__ */ jsx(FileText, {
1604
- "aria-hidden": true,
1605
- className: "size-5 shrink-0 text-chat-muted"
2619
+ children: [/* @__PURE__ */ jsx(IconSlot, {
2620
+ className: "text-chat-muted [&_svg]:size-5",
2621
+ children: icons.file
1606
2622
  }), /* @__PURE__ */ jsx("span", {
1607
- className: "line-clamp-3 text-xs break-words text-chat-fg",
2623
+ className: "line-clamp-3 text-(length:--chat-ui-font-size-sm) leading-4 break-words text-chat-fg",
1608
2624
  children: a.name
1609
2625
  })]
1610
2626
  })
@@ -1613,29 +2629,29 @@ function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", classNam
1613
2629
  className: "absolute inset-x-2 bottom-2 flex items-end justify-between gap-1",
1614
2630
  children: [/* @__PURE__ */ jsx("span", {
1615
2631
  className: clsx("rounded-[0.3rem] border bg-chat-bg px-1.5 py-0.5 text-[0.625rem] leading-none tracking-wide uppercase", failed ? "border-chat-danger text-chat-danger" : "border-chat-border-strong text-chat-muted"),
1616
- children: failed ? "Failed" : pasted ? "Pasted" : extension(a.name) ?? "File"
2632
+ children: failed ? labels.failed : pasted ? labels.pasted : extension(a.name) ?? labels.file
1617
2633
  }), pasted && onOpen && /* @__PURE__ */ jsx(IconButton, {
1618
- label: "Edit pasted text",
2634
+ label: labels.editPastedText,
1619
2635
  size: "sm",
1620
2636
  onPress: onOpen,
1621
2637
  className: "size-6 border border-chat-border-strong bg-chat-bg [&_svg]:size-3.5 [@media(pointer:coarse)]:size-7!",
1622
- children: /* @__PURE__ */ jsx(Pencil, {})
2638
+ children: icons.edit
1623
2639
  })]
1624
2640
  }),
1625
2641
  pasted && onOpen && !onRemove && /* @__PURE__ */ jsx("button", {
1626
2642
  type: "button",
1627
- "aria-label": "View pasted text",
2643
+ "aria-label": labels.viewPastedText,
1628
2644
  onClick: onOpen,
1629
2645
  className: "absolute inset-0 cursor-pointer rounded-[inherit] outline-none focus-visible:ring-2 focus-visible:ring-chat-focus"
1630
2646
  }),
1631
2647
  onRemove && /* @__PURE__ */ jsx(IconButton, {
1632
- label: `Remove ${a.name}`,
2648
+ label: labels.removeAttachment(a.name),
1633
2649
  variant: "subtle",
1634
2650
  size: "sm",
1635
2651
  tooltip: false,
1636
2652
  onPress: onRemove,
1637
2653
  className: clsx("absolute -top-2 -right-2 size-6 rounded-full border border-chat-border-strong [&_svg]:size-3.5 [@media(pointer:coarse)]:size-7!", "opacity-0 group-hover/card:opacity-100 group-focus-within/card:opacity-100 data-focus-visible:opacity-100 [@media(hover:none)]:opacity-100"),
1638
- children: /* @__PURE__ */ jsx(X, {})
2654
+ children: icons.remove
1639
2655
  })
1640
2656
  ]
1641
2657
  });
@@ -1643,9 +2659,10 @@ function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", classNam
1643
2659
  /** The composer's row of cards above the text. Renders nothing when empty. */
1644
2660
  function AttachmentRow({ attachments, onRemove, onEditText }) {
1645
2661
  const [editing, setEditing] = useState();
2662
+ const labels = useLabels();
1646
2663
  if (attachments.length === 0) return null;
1647
2664
  return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("ul", {
1648
- "aria-label": "Attachments",
2665
+ "aria-label": labels.attachments,
1649
2666
  className: "flex gap-2.5 overflow-x-auto px-0.5 pt-2.5 pb-1 [scrollbar-width:thin]",
1650
2667
  children: attachments.map((a) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(AttachmentCard, {
1651
2668
  attachment: {
@@ -1662,7 +2679,9 @@ function AttachmentRow({ attachments, onRemove, onEditText }) {
1662
2679
  })] });
1663
2680
  }
1664
2681
  /** Shows pasted text in a modal, editable when `onSave` is set. */
1665
- function PastedTextDialog({ text, onClose, onSave, title = "Pasted text" }) {
2682
+ function PastedTextDialog({ text, onClose, onSave, title }) {
2683
+ const labels = useLabels();
2684
+ title ??= labels.pastedText;
1666
2685
  const [draft, setDraft] = useState(text ?? "");
1667
2686
  useEffect(() => setDraft(text ?? ""), [text]);
1668
2687
  return /* @__PURE__ */ jsxs(Modal, {
@@ -1674,34 +2693,34 @@ function PastedTextDialog({ text, onClose, onSave, title = "Pasted text" }) {
1674
2693
  onChange: (e) => setDraft(e.target.value),
1675
2694
  readOnly: !onSave,
1676
2695
  "aria-label": title,
1677
- className: "mx-5 min-h-64 flex-1 resize-none rounded-chat-md border border-chat-border bg-chat-bg p-3 font-chat-mono text-sm leading-relaxed text-chat-fg outline-none focus:border-chat-border-strong [scrollbar-width:thin]"
2696
+ className: "mx-5 min-h-64 flex-1 resize-none rounded-chat-md border border-chat-border bg-chat-bg p-3 font-chat-mono text-(length:--chat-ui-font-size) leading-relaxed text-chat-fg outline-none focus:border-chat-border-strong [scrollbar-width:thin]"
1678
2697
  }), /* @__PURE__ */ jsxs("div", {
1679
2698
  className: "flex justify-end gap-2 px-5 py-4",
1680
2699
  children: [/* @__PURE__ */ jsx(Button, {
1681
2700
  variant: "subtle",
1682
2701
  onPress: onClose,
1683
- children: onSave ? "Cancel" : "Close"
2702
+ children: onSave ? labels.cancel : labels.close
1684
2703
  }), onSave && /* @__PURE__ */ jsx(Button, {
1685
2704
  variant: "primary",
1686
2705
  onPress: () => {
1687
2706
  onSave(draft);
1688
2707
  onClose();
1689
2708
  },
1690
- children: "Save"
2709
+ children: labels.save
1691
2710
  })]
1692
2711
  })]
1693
2712
  });
1694
2713
  }
1695
2714
  // src/components/messages.tsx
1696
2715
  /** "just now", "5 minutes ago", "21 hours ago", "yesterday", then a date. */
1697
- function formatRelativeTime(iso, now = Date.now()) {
2716
+ function formatRelativeTime(iso, now = Date.now(), labels = defaultLabels) {
1698
2717
  const seconds = (new Date(iso).getTime() - now) / 1e3;
1699
- const rtf = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
1700
- if (Math.abs(seconds) < 60) return "just now";
2718
+ const rtf = new Intl.RelativeTimeFormat(labels.locale, { numeric: "auto" });
2719
+ if (Math.abs(seconds) < 60) return labels.justNow;
1701
2720
  if (Math.abs(seconds) < 3600) return rtf.format(Math.round(seconds / 60), "minute");
1702
2721
  if (Math.abs(seconds) < 86400) return rtf.format(Math.round(seconds / 3600), "hour");
1703
2722
  if (Math.abs(seconds) < 604800) return rtf.format(Math.round(seconds / 86400), "day");
1704
- return new Date(iso).toLocaleDateString(void 0, {
2723
+ return new Date(iso).toLocaleDateString(labels.locale, {
1705
2724
  month: "short",
1706
2725
  day: "numeric"
1707
2726
  });
@@ -1711,21 +2730,25 @@ function messageText(message) {
1711
2730
  return message.parts.map((p) => p.type === "text" ? p.text : p.type === "custom" ? p.text ?? "" : "").filter(Boolean).join("\n\n");
1712
2731
  }
1713
2732
  function CopyButton({ text }) {
2733
+ const icons = useIcons();
2734
+ const labels = useLabels();
1714
2735
  const [copied, setCopied] = useState(false);
1715
2736
  return /* @__PURE__ */ jsx(IconButton, {
1716
- label: copied ? "Copied" : "Copy",
2737
+ label: copied ? labels.copied : labels.copy,
1717
2738
  size: "sm",
1718
2739
  onPress: () => {
1719
2740
  navigator.clipboard?.writeText(text);
1720
2741
  setCopied(true);
1721
2742
  setTimeout(() => setCopied(false), 1500);
1722
2743
  },
1723
- children: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {})
2744
+ children: copied ? icons.copied : icons.copy
1724
2745
  });
1725
2746
  }
1726
- const timeClass = "px-1 font-chat-sans text-xs text-chat-subtle";
2747
+ const timeClass = "px-1 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle";
1727
2748
  /** The user's turn: a bubble on the right, with time, retry, edit and copy under it on hover. */
1728
- function UserMessage({ message, onRetry, onEdit, actions, className }) {
2749
+ function UserMessage({ message, onRetry, onEdit, actions, copy = true, time = true, className }) {
2750
+ const icons = useIcons();
2751
+ const labels = useLabels();
1729
2752
  const text = messageText(message);
1730
2753
  const attachments = message.parts.filter((p) => p.type === "attachment");
1731
2754
  const [viewing, setViewing] = useState();
@@ -1733,7 +2756,7 @@ function UserMessage({ message, onRetry, onEdit, actions, className }) {
1733
2756
  className: clsx("group/message flex flex-col items-end", className),
1734
2757
  children: [
1735
2758
  attachments.length > 0 && /* @__PURE__ */ jsx("ul", {
1736
- "aria-label": "Attachments",
2759
+ "aria-label": labels.attachments,
1737
2760
  className: "mb-2 flex max-w-[85%] flex-wrap justify-end gap-2",
1738
2761
  children: attachments.map((a) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(AttachmentCard, {
1739
2762
  attachment: a,
@@ -1745,41 +2768,73 @@ function UserMessage({ message, onRetry, onEdit, actions, className }) {
1745
2768
  text: viewing,
1746
2769
  onClose: () => setViewing(void 0)
1747
2770
  }),
1748
- text && /* @__PURE__ */ jsx("div", {
1749
- className: "max-w-[85%] rounded-chat-lg bg-chat-bubble px-4 py-2.5 font-chat-sans text-[0.9375rem] leading-relaxed whitespace-pre-wrap text-chat-fg",
1750
- children: text
1751
- }),
2771
+ text && /* @__PURE__ */ jsx(UserBubble, { text }),
1752
2772
  /* @__PURE__ */ jsxs("div", {
1753
2773
  className: "mt-1 flex items-center gap-0.5 opacity-0 transition-opacity group-hover/message:opacity-100 group-focus-within/message:opacity-100 [@media(hover:none)]:opacity-100",
1754
2774
  children: [
1755
- /* @__PURE__ */ jsx("span", {
2775
+ time && /* @__PURE__ */ jsx("span", {
1756
2776
  className: timeClass,
1757
- children: formatRelativeTime(message.createdAt)
2777
+ children: formatRelativeTime(message.createdAt, void 0, labels)
1758
2778
  }),
1759
2779
  onRetry && /* @__PURE__ */ jsx(IconButton, {
1760
- label: "Retry",
2780
+ label: labels.retry,
1761
2781
  size: "sm",
1762
2782
  onPress: onRetry,
1763
- children: /* @__PURE__ */ jsx(RefreshCw, {})
2783
+ children: icons.retry
1764
2784
  }),
1765
2785
  onEdit && /* @__PURE__ */ jsx(IconButton, {
1766
- label: "Edit",
2786
+ label: labels.edit,
1767
2787
  size: "sm",
1768
2788
  onPress: onEdit,
1769
- children: /* @__PURE__ */ jsx(Pencil, {})
2789
+ children: icons.edit
1770
2790
  }),
1771
- /* @__PURE__ */ jsx(CopyButton, { text }),
2791
+ copy && /* @__PURE__ */ jsx(CopyButton, { text }),
1772
2792
  actions
1773
2793
  ]
1774
2794
  })
1775
2795
  ]
1776
2796
  });
1777
2797
  }
2798
+ /** Taller than this (px), a user message folds to this height with "Show more", like a long paste. */
2799
+ const FOLD_HEIGHT = 240;
2800
+ function UserBubble({ text }) {
2801
+ const body = useRef(null);
2802
+ const [long, setLong] = useState(false);
2803
+ const [open, setOpen] = useState(false);
2804
+ const labels = useLabels();
2805
+ useLayoutEffect(() => {
2806
+ const el = body.current;
2807
+ if (!el) return;
2808
+ const measure = () => setLong(el.scrollHeight > 264);
2809
+ measure();
2810
+ const observer = new ResizeObserver(measure);
2811
+ observer.observe(el);
2812
+ return () => observer.disconnect();
2813
+ }, [text]);
2814
+ const folded = long && !open;
2815
+ return /* @__PURE__ */ jsxs("div", {
2816
+ className: "max-w-[85%] rounded-chat-lg bg-chat-bubble px-4 py-2.5 font-chat-sans text-(length:--chat-bubble-font-size) leading-relaxed text-chat-fg",
2817
+ children: [/* @__PURE__ */ jsx("div", {
2818
+ ref: body,
2819
+ style: folded ? { maxHeight: FOLD_HEIGHT } : void 0,
2820
+ className: clsx("whitespace-pre-wrap", folded && "overflow-hidden mask-b-from-60% mask-b-to-100%"),
2821
+ children: text
2822
+ }), long && /* @__PURE__ */ jsx("button", {
2823
+ type: "button",
2824
+ "aria-expanded": open,
2825
+ onClick: () => setOpen(!open),
2826
+ className: "mt-1.5 cursor-pointer rounded-chat-sm text-(length:--chat-ui-font-size) leading-5 text-chat-muted outline-none hover:text-chat-fg focus-visible:ring-2 focus-visible:ring-chat-focus",
2827
+ children: open ? labels.showLess : labels.showMore
2828
+ })]
2829
+ });
2830
+ }
1778
2831
  /**
1779
2832
  * The assistant's turn: activity groups (each with its own token count), markdown, and other parts in
1780
2833
  * order. When done, copy / read aloud / feedback / retry and the time sit under it.
1781
2834
  */
1782
- function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPart, actions, className }) {
2835
+ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPart, actions, copy = true, time = true, className }) {
2836
+ const icons = useIcons();
2837
+ const labels = useLabels();
1783
2838
  const streaming = message.status === "streaming";
1784
2839
  const views = useToolViews();
1785
2840
  const blocks = toBlocks(message.parts, (part) => views[part.name]?.Block !== void 0);
@@ -1787,56 +2842,66 @@ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPar
1787
2842
  className: clsx("group/message flex flex-col gap-4", className),
1788
2843
  children: [
1789
2844
  blocks.map((block) => {
1790
- if (block.kind === "activity") return /* @__PURE__ */ jsx(ActivityGroup, { parts: block.parts }, block.id);
2845
+ if (block.kind === "activity") {
2846
+ const cards = block.parts.filter((p) => p.type === "tool_call" && views[p.name]?.Card);
2847
+ if (!cards.length) return /* @__PURE__ */ jsx(ActivityGroup, { parts: block.parts }, block.id);
2848
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ActivityGroup, { parts: block.parts }), cards.map((p) => {
2849
+ if (p.type !== "tool_call") return null;
2850
+ const Card = views[p.name].Card;
2851
+ return /* @__PURE__ */ jsx(Card, { part: p }, p.id);
2852
+ })] }, block.id);
2853
+ }
1791
2854
  if (block.kind === "tool") {
1792
2855
  const ToolBlock = views[block.part.name].Block;
1793
2856
  return /* @__PURE__ */ jsx(ToolBlock, { part: block.part }, block.id);
1794
2857
  }
1795
2858
  const part = block.part;
1796
2859
  if (part.type === "text") return /* @__PURE__ */ jsx(Markdown, {
2860
+ partId: part.id,
1797
2861
  streaming: part.status === "streaming",
1798
2862
  children: part.text
1799
2863
  }, part.id);
1800
2864
  const custom = renderPart?.(part);
1801
2865
  if (custom) return /* @__PURE__ */ jsx("div", { children: custom }, part.id);
2866
+ if (part.type === "widget") return /* @__PURE__ */ jsx(WidgetPartView, { part }, part.id);
1802
2867
  if (part.type === "custom" && part.text) return /* @__PURE__ */ jsx(Markdown, { children: part.text }, part.id);
1803
2868
  return null;
1804
2869
  }),
1805
2870
  message.status === "error" && /* @__PURE__ */ jsx("p", {
1806
- className: "font-chat-sans text-sm text-chat-danger",
1807
- children: message.error ?? "Something went wrong."
2871
+ className: "font-chat-sans text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
2872
+ children: message.error ?? labels.somethingWentWrong
1808
2873
  }),
1809
2874
  streaming ? /* @__PURE__ */ jsx("div", { className: "h-7" }) : /* @__PURE__ */ jsxs("div", {
1810
2875
  className: "-ml-1.5 flex h-7 items-center gap-0.5",
1811
2876
  children: [
1812
- /* @__PURE__ */ jsx(CopyButton, { text: messageText(message) }),
2877
+ copy && /* @__PURE__ */ jsx(CopyButton, { text: messageText(message) }),
1813
2878
  onReadAloud && /* @__PURE__ */ jsx(IconButton, {
1814
- label: "Read aloud",
2879
+ label: labels.readAloud,
1815
2880
  size: "sm",
1816
2881
  onPress: onReadAloud,
1817
- children: /* @__PURE__ */ jsx(Volume2, {})
2882
+ children: icons.speak
1818
2883
  }),
1819
2884
  onFeedback && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(IconButton, {
1820
- label: "Good response",
2885
+ label: labels.goodResponse,
1821
2886
  size: "sm",
1822
2887
  onPress: () => onFeedback("good"),
1823
- children: /* @__PURE__ */ jsx(ThumbsUp, {})
2888
+ children: icons.thumbsUp
1824
2889
  }), /* @__PURE__ */ jsx(IconButton, {
1825
- label: "Bad response",
2890
+ label: labels.badResponse,
1826
2891
  size: "sm",
1827
2892
  onPress: () => onFeedback("bad"),
1828
- children: /* @__PURE__ */ jsx(ThumbsDown, {})
2893
+ children: icons.thumbsDown
1829
2894
  })] }),
1830
2895
  onRetry && /* @__PURE__ */ jsx(IconButton, {
1831
- label: "Retry",
2896
+ label: labels.retry,
1832
2897
  size: "sm",
1833
2898
  onPress: onRetry,
1834
- children: /* @__PURE__ */ jsx(RefreshCw, {})
2899
+ children: icons.retry
1835
2900
  }),
1836
2901
  actions,
1837
- /* @__PURE__ */ jsx("span", {
2902
+ time && /* @__PURE__ */ jsx("span", {
1838
2903
  className: timeClass,
1839
- children: formatRelativeTime(message.createdAt)
2904
+ children: formatRelativeTime(message.createdAt, void 0, labels)
1840
2905
  })
1841
2906
  ]
1842
2907
  })
@@ -1849,7 +2914,9 @@ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPar
1849
2914
  * view's `Detail` when it has one, else the input. Replying in the composer instead declines it.
1850
2915
  */
1851
2916
  function ApprovalCard({ approval, onRespond, className }) {
2917
+ const icons = useIcons();
1852
2918
  const { part } = approval;
2919
+ const labels = useLabels();
1853
2920
  const view = useToolView(part.name);
1854
2921
  const Detail = view.Detail;
1855
2922
  const preview = {
@@ -1858,21 +2925,17 @@ function ApprovalCard({ approval, onRespond, className }) {
1858
2925
  };
1859
2926
  return /* @__PURE__ */ jsxs("div", {
1860
2927
  role: "alertdialog",
1861
- "aria-label": `Allow ${part.name}?`,
2928
+ "aria-label": labels.allowTool(part.name),
1862
2929
  className: clsx("flex flex-col gap-3 rounded-chat-lg border border-chat-border-strong bg-chat-surface p-3 font-chat-sans text-chat-fg shadow-chat-popover", className),
1863
2930
  children: [
1864
2931
  /* @__PURE__ */ jsxs("div", {
1865
- className: "flex items-center gap-2 text-sm",
1866
- children: [/* @__PURE__ */ jsx(ShieldQuestion, {
1867
- "aria-hidden": true,
1868
- className: "size-4 shrink-0 text-chat-accent"
2932
+ className: "flex items-center gap-2 text-(length:--chat-ui-font-size) leading-5",
2933
+ children: [/* @__PURE__ */ jsx(IconSlot, {
2934
+ className: "text-chat-accent [&_svg]:size-4",
2935
+ children: icons.approval
1869
2936
  }), /* @__PURE__ */ jsx("span", {
1870
2937
  className: "min-w-0 flex-1 font-medium",
1871
- children: view.ask?.(part) ?? /* @__PURE__ */ jsxs(Fragment, { children: [
1872
- "Allow ",
1873
- activityLabel(part, view),
1874
- "?"
1875
- ] })
2938
+ children: view.ask?.(part) ?? labels.allowAction(activityLabel(part, labels, view))
1876
2939
  })]
1877
2940
  }),
1878
2941
  /* @__PURE__ */ jsx("div", {
@@ -1889,12 +2952,12 @@ function ApprovalCard({ approval, onRespond, className }) {
1889
2952
  variant: "ghost",
1890
2953
  className: "border border-chat-border-strong",
1891
2954
  onPress: () => onRespond(part.id, false),
1892
- children: "Deny"
2955
+ children: labels.deny
1893
2956
  }), /* @__PURE__ */ jsx(Button, {
1894
2957
  variant: "primary",
1895
2958
  autoFocus: true,
1896
2959
  onPress: () => onRespond(part.id, true),
1897
- children: "Allow"
2960
+ children: labels.allow
1898
2961
  })]
1899
2962
  })
1900
2963
  ]
@@ -1907,7 +2970,8 @@ const DEFAULT_COLOR = "var(--chat-subtle)";
1907
2970
  * What fills the context window: a usage line, a segmented bar, one row per category with its share, and
1908
2971
  * expandable lists (each MCP tool, each memory file). Deferred categories are listed but not counted.
1909
2972
  */
1910
- function ContextPanel({ usage, title = "Context", defaultExpanded = true, className }) {
2973
+ function ContextPanel({ usage, title, defaultExpanded = true, className }) {
2974
+ const labels = useLabels();
1911
2975
  const counted = usage.categories.filter((c) => !c.deferred);
1912
2976
  const deferred = usage.categories.filter((c) => c.deferred);
1913
2977
  const used = counted.reduce((sum, c) => sum + (c.reserved ? 0 : c.tokens), 0);
@@ -1915,16 +2979,16 @@ function ContextPanel({ usage, title = "Context", defaultExpanded = true, classN
1915
2979
  const free = Math.max(usage.limit - used - reserved, 0);
1916
2980
  const share = (tokens) => formatShare(tokens / usage.limit);
1917
2981
  return /* @__PURE__ */ jsxs(Disclosure, {
1918
- summary: title,
2982
+ summary: title ?? labels.context,
1919
2983
  defaultExpanded,
1920
2984
  className: clsx("font-chat-sans text-chat-fg", className),
1921
- summaryClassName: "text-[0.9375rem] text-chat-fg",
2985
+ summaryClassName: "text-(length:--chat-activity-font-size) text-chat-fg",
1922
2986
  panelClassName: "pt-3",
1923
2987
  children: [
1924
2988
  /* @__PURE__ */ jsxs("div", {
1925
- className: "mb-2 flex items-baseline justify-between text-[0.9375rem]",
1926
- children: [/* @__PURE__ */ jsxs("span", { children: [Math.round(used / usage.limit * 100), "% used"] }), /* @__PURE__ */ jsxs("span", {
1927
- className: "text-sm text-chat-muted tabular-nums",
2989
+ className: "mb-2 flex items-baseline justify-between text-(length:--chat-activity-font-size)",
2990
+ children: [/* @__PURE__ */ jsx("span", { children: labels.percentUsed(Math.round(used / usage.limit * 100)) }), /* @__PURE__ */ jsxs("span", {
2991
+ className: "text-(length:--chat-ui-font-size) leading-5 text-chat-muted tabular-nums",
1928
2992
  children: [
1929
2993
  formatTokens(used),
1930
2994
  " / ",
@@ -1934,7 +2998,7 @@ function ContextPanel({ usage, title = "Context", defaultExpanded = true, classN
1934
2998
  }),
1935
2999
  /* @__PURE__ */ jsx("div", {
1936
3000
  role: "meter",
1937
- "aria-label": "Context used",
3001
+ "aria-label": labels.contextUsed,
1938
3002
  "aria-valuemin": 0,
1939
3003
  "aria-valuemax": usage.limit,
1940
3004
  "aria-valuenow": used,
@@ -1956,7 +3020,7 @@ function ContextPanel({ usage, title = "Context", defaultExpanded = true, classN
1956
3020
  }, c.id)), /* @__PURE__ */ jsx(CategoryRow, {
1957
3021
  category: {
1958
3022
  id: "free",
1959
- label: "Free space",
3023
+ label: labels.freeSpace,
1960
3024
  tokens: free,
1961
3025
  color: FREE_COLOR
1962
3026
  },
@@ -1987,7 +3051,7 @@ function CategoryRow({ category, share }) {
1987
3051
  }) })
1988
3052
  }) : category.label;
1989
3053
  return /* @__PURE__ */ jsxs("li", {
1990
- className: "flex items-center gap-2.5 text-sm",
3054
+ className: "flex items-center gap-2.5 text-(length:--chat-ui-font-size) leading-5",
1991
3055
  children: [
1992
3056
  /* @__PURE__ */ jsx("span", {
1993
3057
  "aria-hidden": true,
@@ -2013,16 +3077,16 @@ function GroupList({ group }) {
2013
3077
  return /* @__PURE__ */ jsx(Disclosure, {
2014
3078
  summary: group.label,
2015
3079
  trailing: /* @__PURE__ */ jsx("span", {
2016
- className: "text-sm text-chat-subtle tabular-nums",
3080
+ className: "text-(length:--chat-ui-font-size) leading-5 text-chat-subtle tabular-nums",
2017
3081
  children: group.items.length
2018
3082
  }),
2019
3083
  indicator: "chevron",
2020
- summaryClassName: "text-sm",
3084
+ summaryClassName: "text-(length:--chat-ui-font-size) leading-5",
2021
3085
  panelClassName: "pt-1.5",
2022
3086
  children: /* @__PURE__ */ jsx("ul", {
2023
3087
  className: "max-h-72 space-y-1 overflow-y-auto pl-6 [scrollbar-color:var(--chat-border-strong)_transparent] [scrollbar-width:thin]",
2024
3088
  children: group.items.map((item) => /* @__PURE__ */ jsxs("li", {
2025
- className: "flex items-center gap-2 text-sm text-chat-muted",
3089
+ className: "flex items-center gap-2 text-(length:--chat-ui-font-size) leading-5 text-chat-muted",
2026
3090
  children: [/* @__PURE__ */ jsx("span", {
2027
3091
  className: "min-w-0 flex-1 truncate",
2028
3092
  children: item.label
@@ -2041,7 +3105,9 @@ function formatShare(fraction) {
2041
3105
  /** Touch keyboards have no easy Shift+Enter, so there Enter adds a line and the button sends. */
2042
3106
  const coarsePointer = () => typeof window !== "undefined" && window.matchMedia?.("(pointer: coarse)").matches;
2043
3107
  /** The chat box: growing textarea, toolbar slots, send / stop. Enter sends, Shift+Enter adds a line. */
2044
- function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder = "Reply…", variant = "docked", leading, trailing, above, onFiles, onLongPaste, longPasteChars = 2e3, allowEmpty = false, submitDisabled = false, isDisabled = false, autoFocus, className }) {
3108
+ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder, variant = "docked", leading, trailing, above, onFiles, onLongPaste, longPasteChars = 2e3, allowEmpty = false, submitDisabled = false, isDisabled = false, autoFocus, className }) {
3109
+ const icons = useIcons();
3110
+ const labels = useLabels();
2045
3111
  const [own, setOwn] = useState("");
2046
3112
  const text = value ?? own;
2047
3113
  const setText = (next) => {
@@ -2114,21 +3180,21 @@ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
2114
3180
  };
2115
3181
  const compact = variant === "compact";
2116
3182
  const action = busy ? /* @__PURE__ */ jsx(IconButton, {
2117
- label: "Stop",
3183
+ label: labels.stop,
2118
3184
  variant: "primary",
2119
3185
  onPress: onStop,
2120
- className: "rounded-chat-md [&_svg]:size-3.5 [&_svg]:fill-current",
2121
- children: /* @__PURE__ */ jsx(Square, {})
3186
+ className: "rounded-[var(--chat-send-radius,var(--chat-radius-md))]! [&_svg]:size-3.5 [&_svg]:fill-current",
3187
+ children: icons.stop
2122
3188
  }) : /* @__PURE__ */ jsx(IconButton, {
2123
- label: "Send",
3189
+ label: labels.send,
2124
3190
  variant: "primary",
2125
3191
  onPress: submit,
2126
3192
  isDisabled: !canSend,
2127
- className: "rounded-chat-md",
2128
- children: /* @__PURE__ */ jsx(ArrowUp, {})
3193
+ className: "rounded-[var(--chat-send-radius,var(--chat-radius-md))]!",
3194
+ children: icons.send
2129
3195
  });
2130
3196
  return /* @__PURE__ */ jsxs("div", {
2131
- className: clsx("flex flex-col rounded-[1.25rem] border border-chat-border bg-chat-surface font-chat-sans shadow-chat-popover", "focus-within:border-chat-border-strong", dragging && "border-chat-accent! border-dashed", compact ? "gap-1 p-2" : "gap-2 p-3", className),
3197
+ className: clsx("flex flex-col rounded-[var(--chat-composer-radius,1.25rem)] border border-chat-border bg-chat-surface font-chat-sans shadow-chat-popover", "focus-within:border-chat-border-strong", dragging && "border-chat-accent! border-dashed", compact ? "gap-1 p-2" : "gap-2 p-3", className),
2132
3198
  ...dropProps,
2133
3199
  onPointerDown: (e) => {
2134
3200
  if (e.target === e.currentTarget) {
@@ -2147,8 +3213,8 @@ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
2147
3213
  onChange: (e) => setText(e.target.value),
2148
3214
  onKeyDown,
2149
3215
  onPaste,
2150
- placeholder,
2151
- "aria-label": "Message",
3216
+ placeholder: placeholder ?? labels.replyPlaceholder,
3217
+ "aria-label": labels.message,
2152
3218
  disabled: isDisabled,
2153
3219
  autoFocus,
2154
3220
  className: clsx("w-full resize-none bg-transparent px-1.5 text-chat-fg outline-none placeholder:text-chat-subtle", "text-base leading-relaxed [scrollbar-width:thin]", compact ? "max-h-[min(10rem,30cqh)] py-1" : variant === "centered" ? "max-h-[min(24rem,45cqh)] min-h-12 pt-1" : "max-h-[min(18rem,35cqh)] min-h-6 pt-1")
@@ -2172,18 +3238,24 @@ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
2172
3238
  });
2173
3239
  }
2174
3240
  /** The `+` button that opens the attach menu above the composer. */
2175
- function AttachButton({ children, label = "Add files and more" }) {
3241
+ function AttachButton({ children, label, icon, className, menuClassName }) {
3242
+ const icons = useIcons();
3243
+ const labels = useLabels();
2176
3244
  return /* @__PURE__ */ jsxs(MenuTrigger, { children: [/* @__PURE__ */ jsx(IconButton, {
2177
- label,
2178
- children: /* @__PURE__ */ jsx(Plus, {})
3245
+ label: label ?? labels.addFiles,
3246
+ ...className && { className },
3247
+ children: icon ?? icons.attach
2179
3248
  }), /* @__PURE__ */ jsx(Popover, {
2180
3249
  placement: "top start",
3250
+ ...menuClassName && { className: menuClassName },
2181
3251
  children
2182
3252
  })] });
2183
3253
  }
2184
3254
  // src/components/model-picker.tsx
2185
3255
  /** "Opus 5.5 High ⌄": the models, an Effort submenu for models that have levels, and More models. */
2186
- function ModelPicker({ models, model, onModelChange, effort, onEffortChange, effortNote = "Higher effort means more thorough responses, but takes longer and uses your limits faster.", footnote, className }) {
3256
+ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, effortNote, footnote, className }) {
3257
+ const icons = useIcons();
3258
+ const labels = useLabels();
2187
3259
  const current = models.find((m) => m.id === model);
2188
3260
  const efforts = current?.efforts ?? [];
2189
3261
  const currentEffort = efforts.find((e) => e.id === (effort ?? current?.defaultEffort));
@@ -2202,7 +3274,7 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
2202
3274
  };
2203
3275
  return /* @__PURE__ */ jsxs(MenuTrigger, { children: [/* @__PURE__ */ jsxs(Button, {
2204
3276
  size: "sm",
2205
- "aria-label": `Model: ${current?.label ?? model}`,
3277
+ "aria-label": labels.modelButton(current?.label ?? model),
2206
3278
  className: clsx("text-chat-muted data-hovered:text-chat-fg", className),
2207
3279
  children: [
2208
3280
  /* @__PURE__ */ jsx("span", {
@@ -2213,16 +3285,16 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
2213
3285
  className: "chat-picker-detail text-chat-muted",
2214
3286
  children: currentEffort.label
2215
3287
  }),
2216
- /* @__PURE__ */ jsx(ChevronDown, {
2217
- "aria-hidden": true,
2218
- className: "size-3.5"
3288
+ /* @__PURE__ */ jsx(IconSlot, {
3289
+ className: "[&_svg]:size-3.5",
3290
+ children: icons.dropdown
2219
3291
  })
2220
3292
  ]
2221
3293
  }), /* @__PURE__ */ jsxs(Popover, {
2222
3294
  placement: "top end",
2223
3295
  className: "w-72",
2224
3296
  children: [/* @__PURE__ */ jsxs(Menu, {
2225
- "aria-label": "Model",
3297
+ "aria-label": labels.model,
2226
3298
  children: [
2227
3299
  /* @__PURE__ */ jsx(MenuSection, {
2228
3300
  selectionMode: "single",
@@ -2234,12 +3306,12 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
2234
3306
  efforts.length > 0 && onEffortChange && /* @__PURE__ */ jsxs(SubmenuTrigger, { children: [/* @__PURE__ */ jsx(MenuItem, {
2235
3307
  id: "__effort",
2236
3308
  detail: currentEffort?.label,
2237
- children: "Effort"
3309
+ children: labels.effort
2238
3310
  }), /* @__PURE__ */ jsxs(Popover, {
2239
3311
  placement: "end top",
2240
3312
  className: "w-80",
2241
- children: [/* @__PURE__ */ jsx(MenuNote, { children: effortNote }), /* @__PURE__ */ jsx(Menu, {
2242
- "aria-label": "Effort",
3313
+ children: [/* @__PURE__ */ jsx(MenuNote, { children: effortNote ?? labels.effortNote }), /* @__PURE__ */ jsx(Menu, {
3314
+ "aria-label": labels.effort,
2243
3315
  selectionMode: "single",
2244
3316
  selectedKeys: currentEffort ? [currentEffort.id] : [],
2245
3317
  onSelectionChange: (keys) => {
@@ -2254,10 +3326,10 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
2254
3326
  className: "flex items-center gap-2",
2255
3327
  children: [
2256
3328
  e.label,
2257
- e.recommended && /* @__PURE__ */ jsx(Badge, { children: "Recommended" }),
3329
+ e.recommended && /* @__PURE__ */ jsx(Badge, { children: labels.recommended }),
2258
3330
  e.warning && /* @__PURE__ */ jsx(Badge, {
2259
3331
  tone: "warning",
2260
- icon: /* @__PURE__ */ jsx(AlertTriangle, {}),
3332
+ icon: icons.warning,
2261
3333
  children: e.warning
2262
3334
  })
2263
3335
  ]
@@ -2267,12 +3339,12 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
2267
3339
  })] }),
2268
3340
  more.length > 0 && /* @__PURE__ */ jsxs(SubmenuTrigger, { children: [/* @__PURE__ */ jsx(MenuItem, {
2269
3341
  id: "__more",
2270
- children: "More models"
3342
+ children: labels.moreModels
2271
3343
  }), /* @__PURE__ */ jsx(Popover, {
2272
3344
  placement: "end top",
2273
3345
  className: "w-72",
2274
3346
  children: /* @__PURE__ */ jsx(Menu, {
2275
- "aria-label": "More models",
3347
+ "aria-label": labels.moreModels,
2276
3348
  selectionMode: "single",
2277
3349
  selectedKeys: [model],
2278
3350
  onSelectionChange: pick,
@@ -2281,7 +3353,7 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
2281
3353
  })] })
2282
3354
  ]
2283
3355
  }), footnote && /* @__PURE__ */ jsx(MenuNote, {
2284
- className: "border-t border-chat-popover-border text-xs",
3356
+ className: "border-t border-chat-popover-border text-(length:--chat-ui-font-size-sm) leading-4",
2285
3357
  children: footnote
2286
3358
  })]
2287
3359
  })] });
@@ -2293,10 +3365,12 @@ const STICK_SLOP = 48;
2293
3365
  * The scrolling conversation. It follows a streaming reply while the reader is at the bottom and leaves
2294
3366
  * them alone once they scroll up, with a button to jump back. Sending a message always jumps down.
2295
3367
  */
2296
- function MessageList({ messages, onRetry, onEdit, onFeedback, renderPart, footer, className }) {
3368
+ function MessageList({ messages, onRetry, onEdit, onFeedback, onReadAloud, actions, renderPart, header, footer, start = "bottom", className }) {
3369
+ const icons = useIcons();
2297
3370
  const scroller = useRef(null);
2298
3371
  const content = useRef(null);
2299
- const stuck = useRef(true);
3372
+ const stuck = useRef(start === "bottom");
3373
+ const labels = useLabels();
2300
3374
  const [atBottom, setAtBottom] = useState(true);
2301
3375
  const toBottom = useCallback((behavior = "auto") => {
2302
3376
  const el = scroller.current;
@@ -2322,7 +3396,13 @@ function MessageList({ messages, onRetry, onEdit, onFeedback, renderPart, footer
2322
3396
  return () => observer.disconnect();
2323
3397
  }, [toBottom]);
2324
3398
  const lastUser = messages.findLast((m) => m.role === "user")?.id;
3399
+ const jumpedFor = useRef(start === "top" ? lastUser : null);
2325
3400
  useLayoutEffect(() => {
3401
+ if (lastUser === jumpedFor.current) {
3402
+ onScroll();
3403
+ return;
3404
+ }
3405
+ jumpedFor.current = lastUser;
2326
3406
  stuck.current = true;
2327
3407
  setAtBottom(true);
2328
3408
  toBottom();
@@ -2336,39 +3416,47 @@ function MessageList({ messages, onRetry, onEdit, onFeedback, renderPart, footer
2336
3416
  children: /* @__PURE__ */ jsxs("div", {
2337
3417
  ref: content,
2338
3418
  className: "flex min-h-full flex-col",
2339
- children: [/* @__PURE__ */ jsx("div", {
3419
+ children: [/* @__PURE__ */ jsxs("div", {
2340
3420
  role: "log",
2341
- "aria-label": "Conversation",
3421
+ "aria-label": labels.conversation,
2342
3422
  className: "mx-auto flex w-full max-w-[calc(var(--chat-content-width)+2*var(--chat-gutter,var(--chat-content-padding)))] flex-1 flex-col gap-6 px-[var(--chat-gutter,var(--chat-content-padding))] pt-6 pb-4",
2343
- children: messages.map((m) => m.role === "user" ? /* @__PURE__ */ jsx(UserMessage, {
3423
+ children: [header, messages.map((m) => m.role === "user" ? /* @__PURE__ */ jsx(UserMessage, {
2344
3424
  message: m,
2345
- ...onEdit && { onEdit: () => onEdit(m.id) }
3425
+ ...onEdit && { onEdit: () => onEdit(m.id) },
3426
+ ...actions?.extra && { actions: actions.extra(m) },
3427
+ ...actions?.copy === false && { copy: false },
3428
+ ...actions?.time === false && { time: false }
2346
3429
  }, m.id) : /* @__PURE__ */ jsx(AssistantMessage, {
2347
3430
  message: m,
2348
3431
  ...onRetry && { onRetry: () => onRetry(m.id) },
2349
3432
  ...onFeedback && { onFeedback: (f) => onFeedback(m.id, f) },
3433
+ ...onReadAloud && { onReadAloud: () => onReadAloud(m.id) },
3434
+ ...actions?.extra && { actions: actions.extra(m) },
3435
+ ...actions?.copy === false && { copy: false },
3436
+ ...actions?.time === false && { time: false },
2350
3437
  ...renderPart && { renderPart }
2351
- }, m.id))
3438
+ }, m.id))]
2352
3439
  }), footer && /* @__PURE__ */ jsx("div", {
2353
3440
  className: "sticky bottom-0",
2354
3441
  children: footer
2355
3442
  })]
2356
3443
  })
2357
3444
  }), !atBottom && /* @__PURE__ */ jsx(IconButton, {
2358
- label: "Scroll to bottom",
3445
+ label: labels.scrollToBottom,
2359
3446
  variant: "subtle",
2360
3447
  onPress: () => {
2361
3448
  stuck.current = true;
2362
3449
  toBottom("smooth");
2363
3450
  },
2364
3451
  className: "absolute bottom-[min(10rem,40%)] left-1/2 -translate-x-1/2 rounded-full border border-chat-border shadow-chat-popover",
2365
- children: /* @__PURE__ */ jsx(ArrowDown, {})
3452
+ children: icons.scrollToBottom
2366
3453
  })]
2367
3454
  });
2368
3455
  }
2369
3456
  // src/components/chat-header.tsx
2370
3457
  /** The bar above a chat: breadcrumb and title with its menu, actions on the right. */
2371
3458
  function ChatHeader({ title, menu, leading, actions, className }) {
3459
+ const icons = useIcons();
2372
3460
  return /* @__PURE__ */ jsxs("header", {
2373
3461
  className: clsx("flex h-12 shrink-0 items-center gap-1 px-3 font-chat-sans", className),
2374
3462
  children: [
@@ -2379,15 +3467,15 @@ function ChatHeader({ title, menu, leading, actions, className }) {
2379
3467
  children: [/* @__PURE__ */ jsx("span", {
2380
3468
  className: "truncate",
2381
3469
  children: title
2382
- }), /* @__PURE__ */ jsx(ChevronDown, {
2383
- "aria-hidden": true,
2384
- className: "size-3.5 shrink-0 text-chat-muted"
3470
+ }), /* @__PURE__ */ jsx(IconSlot, {
3471
+ className: "text-chat-muted [&_svg]:size-3.5",
3472
+ children: icons.dropdown
2385
3473
  })]
2386
3474
  }), /* @__PURE__ */ jsx(Popover, {
2387
3475
  placement: "bottom start",
2388
3476
  children: menu
2389
3477
  })] }) : /* @__PURE__ */ jsx("h1", {
2390
- className: "min-w-0 truncate px-2 text-sm text-chat-fg",
3478
+ className: "min-w-0 truncate px-2 text-(length:--chat-ui-font-size) leading-5 text-chat-fg",
2391
3479
  children: title
2392
3480
  }),
2393
3481
  /* @__PURE__ */ jsx("div", {
@@ -2399,6 +3487,8 @@ function ChatHeader({ title, menu, leading, actions, className }) {
2399
3487
  }
2400
3488
  /** Pin, Rename, Add to project (searchable), Share, Delete. Each shows only when its handler is set. */
2401
3489
  function ChatTitleMenu({ pinned, onPin, onRename, projects, onAddToProject, onCreateProject, onShare, onDelete, children }) {
3490
+ const icons = useIcons();
3491
+ const labels = useLabels();
2402
3492
  const act = (key) => {
2403
3493
  if (key === "pin") onPin?.();
2404
3494
  else if (key === "rename") onRename?.();
@@ -2406,39 +3496,39 @@ function ChatTitleMenu({ pinned, onPin, onRename, projects, onAddToProject, onCr
2406
3496
  else if (key === "delete") onDelete?.();
2407
3497
  };
2408
3498
  return /* @__PURE__ */ jsxs(Menu, {
2409
- "aria-label": "Chat actions",
3499
+ "aria-label": labels.chatActions,
2410
3500
  className: "min-w-44",
2411
3501
  onAction: act,
2412
3502
  children: [
2413
3503
  onPin && /* @__PURE__ */ jsx(MenuItem, {
2414
3504
  id: "pin",
2415
- icon: pinned ? /* @__PURE__ */ jsx(PinOff, {}) : /* @__PURE__ */ jsx(Pin, {}),
3505
+ icon: pinned ? icons.unpin : icons.pin,
2416
3506
  shortcut: "P",
2417
- children: pinned ? "Unpin" : "Pin"
3507
+ children: pinned ? labels.unpin : labels.pin
2418
3508
  }),
2419
3509
  onRename && /* @__PURE__ */ jsx(MenuItem, {
2420
3510
  id: "rename",
2421
- icon: /* @__PURE__ */ jsx(Pencil, {}),
3511
+ icon: icons.rename,
2422
3512
  shortcut: "R",
2423
- children: "Rename"
3513
+ children: labels.rename
2424
3514
  }),
2425
3515
  onAddToProject && /* @__PURE__ */ jsxs(SubmenuTrigger, { children: [/* @__PURE__ */ jsx(MenuItem, {
2426
3516
  id: "project",
2427
- icon: /* @__PURE__ */ jsx(FolderPlus, {}),
2428
- children: "Add to project"
3517
+ icon: icons.addToProject,
3518
+ children: labels.addToProject
2429
3519
  }), /* @__PURE__ */ jsx(Popover, {
2430
3520
  placement: "end top",
2431
3521
  className: "w-60",
2432
3522
  children: /* @__PURE__ */ jsx(MenuSearch, {
2433
- placeholder: "Search or create a project",
3523
+ placeholder: labels.searchProjects,
2434
3524
  children: /* @__PURE__ */ jsxs(Menu, {
2435
- "aria-label": "Projects",
3525
+ "aria-label": labels.projects,
2436
3526
  className: "max-h-72",
2437
3527
  onAction: (key) => key === "__new" ? onCreateProject?.() : onAddToProject(String(key)),
2438
3528
  children: [onCreateProject && /* @__PURE__ */ jsx(MenuItem, {
2439
3529
  id: "__new",
2440
- icon: /* @__PURE__ */ jsx(Plus, {}),
2441
- children: "New project"
3530
+ icon: icons.add,
3531
+ children: labels.newProject
2442
3532
  }), (projects ?? []).map((p) => /* @__PURE__ */ jsx(MenuItem, {
2443
3533
  id: p.id,
2444
3534
  children: p.name
@@ -2448,28 +3538,50 @@ function ChatTitleMenu({ pinned, onPin, onRename, projects, onAddToProject, onCr
2448
3538
  })] }),
2449
3539
  onShare && /* @__PURE__ */ jsx(MenuItem, {
2450
3540
  id: "share",
2451
- icon: /* @__PURE__ */ jsx(Share, {}),
2452
- children: "Share chat"
3541
+ icon: icons.share,
3542
+ children: labels.shareChat
2453
3543
  }),
2454
3544
  children,
2455
3545
  onDelete && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MenuSeparator, {}), /* @__PURE__ */ jsx(MenuItem, {
2456
3546
  id: "delete",
2457
- icon: /* @__PURE__ */ jsx(Trash2, {}),
3547
+ icon: icons.delete,
2458
3548
  shortcut: "D",
2459
3549
  tone: "danger",
2460
- children: "Delete"
3550
+ children: labels.delete
2461
3551
  })] })
2462
3552
  ]
2463
3553
  });
2464
3554
  }
3555
+ // src/components/chat-notice.tsx
3556
+ /** A bordered note in the conversation column: "Shared by Alex. This is a copy of a chat…" with actions under it. */
3557
+ function ChatNotice({ children, actions, icon, className }) {
3558
+ const icons = useIcons();
3559
+ const shown = icon === void 0 ? icons.info : icon;
3560
+ return /* @__PURE__ */ jsxs("div", {
3561
+ role: "note",
3562
+ className: clsx("flex gap-3 rounded-chat-lg border border-(--chat-notice-border,var(--chat-border)) bg-(--chat-notice-bg,transparent) px-4 py-3.5 font-chat-sans text-(length:--chat-ui-font-size) leading-relaxed text-chat-fg", className),
3563
+ children: [shown && /* @__PURE__ */ jsx("span", {
3564
+ className: "mt-0.5 shrink-0 text-chat-muted [&_svg]:size-4",
3565
+ children: shown
3566
+ }), /* @__PURE__ */ jsxs("div", {
3567
+ className: "min-w-0 flex-1",
3568
+ children: [/* @__PURE__ */ jsx("div", { children }), actions && /* @__PURE__ */ jsx("div", {
3569
+ className: "mt-2.5 flex flex-wrap gap-2",
3570
+ children: actions
3571
+ })]
3572
+ })]
3573
+ });
3574
+ }
2465
3575
  // src/components/chat-list.tsx
2466
3576
  const isProject = (item) => "type" in item && item.type === "project";
2467
- const rowClass = clsx("group/row relative flex h-8 w-full min-w-0 cursor-pointer items-center gap-2 rounded-chat-sm px-2 text-left font-chat-sans text-sm text-chat-fg outline-none", "data-hovered:bg-chat-hover data-pressed:bg-chat-selected data-focus-visible:ring-2 data-focus-visible:ring-chat-focus data-focus-visible:ring-inset", "[@media(pointer:coarse)]:h-10");
3577
+ const rowClass = clsx("group/row relative flex h-8 w-full min-w-0 cursor-pointer items-center gap-2 rounded-chat-sm px-2 text-left font-chat-sans text-(length:--chat-ui-font-size) leading-5 text-chat-fg outline-none", "data-hovered:bg-chat-hover data-pressed:bg-chat-selected data-focus-visible:ring-2 data-focus-visible:ring-chat-focus data-focus-visible:ring-inset", "[@media(pointer:coarse)]:h-10");
2468
3578
  /**
2469
3579
  * The chat picker: sections of chats and projects, collapsible, with an active row, running and unread
2470
3580
  * markers and a per-row menu. Usable on its own (in a host's own sidebar) or inside `Sidebar`.
2471
3581
  */
2472
3582
  function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }) {
3583
+ const icons = useIcons();
3584
+ const labels = useLabels();
2473
3585
  const row = (chat, nested = false) => /* @__PURE__ */ jsx(ChatRow, {
2474
3586
  chat,
2475
3587
  active: chat.id === activeId,
@@ -2483,13 +3595,13 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
2483
3595
  row
2484
3596
  }, item.id) : row(item));
2485
3597
  return /* @__PURE__ */ jsx("nav", {
2486
- "aria-label": props["aria-label"] ?? "Chats",
3598
+ "aria-label": props["aria-label"] ?? labels.chats,
2487
3599
  className: clsx("flex flex-col gap-3", className),
2488
3600
  children: sections.map((section) => section.title ? /* @__PURE__ */ jsxs(Disclosure$1, {
2489
3601
  defaultExpanded: section.defaultExpanded ?? true,
2490
3602
  className: "group/section",
2491
3603
  children: [/* @__PURE__ */ jsxs(Heading, {
2492
- className: "m-0 flex h-7 items-center gap-1 pr-1 pl-2 font-chat-sans text-xs font-normal text-chat-subtle",
3604
+ className: "m-0 flex h-7 items-center gap-1 pr-1 pl-2 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 font-normal text-chat-subtle",
2493
3605
  children: [
2494
3606
  /* @__PURE__ */ jsxs(Button$1, {
2495
3607
  slot: "trigger",
@@ -2497,9 +3609,9 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
2497
3609
  children: [/* @__PURE__ */ jsx("span", {
2498
3610
  className: "truncate",
2499
3611
  children: section.title
2500
- }), /* @__PURE__ */ jsx(ChevronRight, {
2501
- "aria-hidden": true,
2502
- className: "size-3 shrink-0 opacity-0 transition group-hover/section:opacity-100 group-aria-expanded/trigger:rotate-90 group-data-focus-visible/trigger:opacity-100 [@media(hover:none)]:opacity-100"
3612
+ }), /* @__PURE__ */ jsx(IconSlot, {
3613
+ className: "opacity-0 transition group-hover/section:opacity-100 group-aria-expanded/trigger:rotate-90 group-data-focus-visible/trigger:opacity-100 [@media(hover:none)]:opacity-100 [&_svg]:size-3",
3614
+ children: icons.expand
2503
3615
  })]
2504
3616
  }),
2505
3617
  section.badge,
@@ -2514,7 +3626,7 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
2514
3626
  }), section.onViewAll && /* @__PURE__ */ jsx(Button$1, {
2515
3627
  onPress: section.onViewAll,
2516
3628
  className: clsx(rowClass, "text-chat-muted"),
2517
- children: "View all"
3629
+ children: labels.viewAll
2518
3630
  })] })]
2519
3631
  }, section.id) : /* @__PURE__ */ jsx("ul", {
2520
3632
  className: "flex flex-col gap-px",
@@ -2523,16 +3635,18 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
2523
3635
  });
2524
3636
  }
2525
3637
  function ChatRow({ chat, active, nested, onSelect, menu }) {
3638
+ const icons = useIcons();
2526
3639
  const [menuOpen, setMenuOpen] = useState(false);
2527
3640
  const { dismiss } = useSidebar();
3641
+ const labels = useLabels();
2528
3642
  const markerClass = "size-2 shrink-0 rounded-full";
2529
3643
  const marker = chat.running ? /* @__PURE__ */ jsx("span", {
2530
3644
  role: "img",
2531
- "aria-label": "Running",
3645
+ "aria-label": labels.running,
2532
3646
  className: clsx(markerClass, "animate-pulse bg-chat-accent motion-reduce:animate-none")
2533
3647
  }) : chat.unread ? /* @__PURE__ */ jsx("span", {
2534
3648
  role: "img",
2535
- "aria-label": "Unread",
3649
+ "aria-label": labels.unread,
2536
3650
  className: clsx(markerClass, "bg-chat-check")
2537
3651
  }) : null;
2538
3652
  const content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
@@ -2557,11 +3671,11 @@ function ChatRow({ chat, active, nested, onSelect, menu }) {
2557
3671
  isOpen: menuOpen,
2558
3672
  onOpenChange: setMenuOpen,
2559
3673
  children: [/* @__PURE__ */ jsx(IconButton, {
2560
- label: `More options for ${chat.title}`,
3674
+ label: labels.moreOptionsFor(chat.title),
2561
3675
  size: "sm",
2562
3676
  tooltip: false,
2563
3677
  className: clsx("absolute top-1/2 right-1 size-6 -translate-y-1/2 [@media(pointer:coarse)]:size-8", menuOpen || active ? "opacity-100" : "opacity-0 group-hover/item:opacity-100 group-focus-within/item:opacity-100 [@media(hover:none)]:opacity-100"),
2564
- children: /* @__PURE__ */ jsx(MoreHorizontal, {})
3678
+ children: icons.more
2565
3679
  }), /* @__PURE__ */ jsx(Popover, {
2566
3680
  placement: "bottom start",
2567
3681
  children: menu
@@ -2570,6 +3684,8 @@ function ChatRow({ chat, active, nested, onSelect, menu }) {
2570
3684
  });
2571
3685
  }
2572
3686
  function ProjectRow({ project, activeId, row }) {
3687
+ const icons = useIcons();
3688
+ const labels = useLabels();
2573
3689
  const holdsActive = project.chats.some((c) => c.id === activeId);
2574
3690
  return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Disclosure$1, {
2575
3691
  defaultExpanded: project.defaultExpanded ?? holdsActive,
@@ -2579,18 +3695,15 @@ function ProjectRow({ project, activeId, row }) {
2579
3695
  slot: "trigger",
2580
3696
  className: rowClass,
2581
3697
  children: [
2582
- isExpanded ? /* @__PURE__ */ jsx(FolderOpen, {
2583
- "aria-hidden": true,
2584
- className: "size-4 shrink-0 text-chat-muted"
2585
- }) : /* @__PURE__ */ jsx(Folder, {
2586
- "aria-hidden": true,
2587
- className: "size-4 shrink-0 text-chat-muted"
3698
+ /* @__PURE__ */ jsx(IconSlot, {
3699
+ className: "text-chat-muted [&_svg]:size-4",
3700
+ children: isExpanded ? icons.folderOpen : icons.folder
2588
3701
  }),
2589
3702
  /* @__PURE__ */ jsx("span", {
2590
3703
  className: "min-w-0 flex-1 truncate",
2591
3704
  children: project.name
2592
3705
  }),
2593
- !isExpanded && project.chats.some((c) => c.unread || c.running) && /* @__PURE__ */ jsx(Badge, { children: "New" })
3706
+ !isExpanded && project.chats.some((c) => c.unread || c.running) && /* @__PURE__ */ jsx(Badge, { children: labels.new })
2594
3707
  ]
2595
3708
  })
2596
3709
  }), /* @__PURE__ */ jsx(DisclosurePanel, { children: /* @__PURE__ */ jsx("ul", {
@@ -2600,7 +3713,11 @@ function ProjectRow({ project, activeId, row }) {
2600
3713
  }) });
2601
3714
  }
2602
3715
  // src/views/chat-view.tsx
2603
- /** Column defaults per density; `contentWidth` / `contentPadding` override them. */
3716
+ /**
3717
+ * Column defaults per density; `contentWidth` / `contentPadding` override them. Only the message size changes
3718
+ * with density: UI text is the same in all three. A theme can set the other text-size variables per density
3719
+ * with `[data-chat-density="panel"] { --chat-ui-font-size: … }`.
3720
+ */
2604
3721
  const densityVars = {
2605
3722
  full: {},
2606
3723
  panel: {
@@ -2615,14 +3732,20 @@ const densityVars = {
2615
3732
  }
2616
3733
  };
2617
3734
  /** A whole chat: header, the conversation, the docked composer. An empty chat shows `emptyState`. */
2618
- function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove, onRetry, onEdit, onFeedback, renderPart, header, composer, emptyState, disclaimer, density = "full", contentWidth, contentPadding, className }) {
3735
+ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove, onRetry, onEdit, onFeedback, onReadAloud, messageActions, renderPart, widgets, header, composer, emptyState, disclaimer, density = "full", contentWidth, contentPadding, className }) {
2619
3736
  const style = {
2620
3737
  ...densityVars[density],
2621
3738
  ...contentWidth && { "--chat-content-width": contentWidth },
2622
3739
  ...contentPadding && { "--chat-content-padding": contentPadding }
2623
3740
  };
3741
+ const host = useMemo(() => ({
3742
+ onPrompt: onSend,
3743
+ onOpenLink: openInNewTab,
3744
+ ...widgets
3745
+ }), [onSend, widgets]);
2624
3746
  const empty = messages.length === 0;
2625
3747
  const [body, short] = useShortPane();
3748
+ const labels = useLabels();
2626
3749
  const pending = onApprove && approvals?.[0];
2627
3750
  const box = (variant) => /* @__PURE__ */ jsx(Composer, {
2628
3751
  variant: density === "inline" || short && !empty ? "compact" : variant,
@@ -2630,7 +3753,7 @@ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove
2630
3753
  onSubmit: onSend,
2631
3754
  ...onStop && { onStop },
2632
3755
  ...composer,
2633
- ...pending && { placeholder: "Or reply directly…" }
3756
+ ...pending && { placeholder: labels.replyDirectly }
2634
3757
  });
2635
3758
  return /* @__PURE__ */ jsxs("div", {
2636
3759
  "data-chat-density": density,
@@ -2645,28 +3768,33 @@ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove
2645
3768
  className: "w-full max-w-(--chat-content-width)",
2646
3769
  children: emptyState ? emptyState(box("centered")) : box("centered")
2647
3770
  })
2648
- }) : /* @__PURE__ */ jsx(MessageList, {
2649
- messages,
2650
- ...onRetry && { onRetry },
2651
- ...onEdit && { onEdit },
2652
- ...onFeedback && { onFeedback },
2653
- ...renderPart && { renderPart },
2654
- footer: /* @__PURE__ */ jsx("div", {
2655
- className: clsx("bg-linear-to-t from-chat-bg from-70% to-transparent px-(--chat-gutter)", short ? "pt-2" : "pt-4"),
2656
- children: /* @__PURE__ */ jsxs("div", {
2657
- className: "mx-auto w-full max-w-(--chat-content-width)",
2658
- children: [
2659
- pending && /* @__PURE__ */ jsx(ApprovalCard, {
2660
- approval: pending,
2661
- onRespond: onApprove,
2662
- className: "mb-2"
2663
- }, pending.part.id),
2664
- box("docked"),
2665
- /* @__PURE__ */ jsx("div", {
2666
- className: clsx("flex items-center justify-center py-1 pb-[max(0.25rem,env(safe-area-inset-bottom))] text-center font-chat-sans text-xs text-chat-subtle", short ? "min-h-2" : "min-h-6"),
2667
- children: disclaimer && !short && /* @__PURE__ */ jsx("span", { children: disclaimer })
2668
- })
2669
- ]
3771
+ }) : /* @__PURE__ */ jsx(WidgetHostProvider, {
3772
+ host,
3773
+ children: /* @__PURE__ */ jsx(MessageList, {
3774
+ messages,
3775
+ ...onRetry && { onRetry },
3776
+ ...onEdit && { onEdit },
3777
+ ...onFeedback && { onFeedback },
3778
+ ...onReadAloud && { onReadAloud },
3779
+ ...messageActions && { actions: messageActions },
3780
+ ...renderPart && { renderPart },
3781
+ footer: /* @__PURE__ */ jsx("div", {
3782
+ className: clsx("bg-linear-to-t from-chat-bg from-70% to-transparent px-(--chat-gutter)", short ? "pt-2" : "pt-4"),
3783
+ children: /* @__PURE__ */ jsxs("div", {
3784
+ className: "mx-auto w-full max-w-(--chat-content-width)",
3785
+ children: [
3786
+ pending && /* @__PURE__ */ jsx(ApprovalCard, {
3787
+ approval: pending,
3788
+ onRespond: onApprove,
3789
+ className: "mb-2"
3790
+ }, pending.part.id),
3791
+ box("docked"),
3792
+ /* @__PURE__ */ jsx("div", {
3793
+ className: clsx("flex items-center justify-center py-1 pb-[max(0.25rem,env(safe-area-inset-bottom))] text-center font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle", short ? "min-h-2" : "min-h-6"),
3794
+ children: disclaimer && !short && /* @__PURE__ */ jsx("span", { children: disclaimer })
3795
+ })
3796
+ ]
3797
+ })
2670
3798
  })
2671
3799
  })
2672
3800
  })
@@ -2703,6 +3831,51 @@ function EmptyState({ greeting, icon, children, footer }) {
2703
3831
  ]
2704
3832
  });
2705
3833
  }
3834
+ // src/views/chat-transcript.tsx
3835
+ /**
3836
+ * A conversation to read, not to continue: a shared chat, an export, a log. Same messages as ChatView,
3837
+ * with no composer, approvals, retry, edit or feedback. Copy stays.
3838
+ */
3839
+ function ChatTranscript({ messages, renderPart, widgets, header, notice, footer, start = "top", density = "full", contentWidth, contentPadding, className }) {
3840
+ const style = {
3841
+ ...densityVars[density],
3842
+ ...contentWidth && { "--chat-content-width": contentWidth },
3843
+ ...contentPadding && { "--chat-content-padding": contentPadding }
3844
+ };
3845
+ const host = useMemo(() => {
3846
+ const { allow, readResource } = { ...widgets };
3847
+ const onOpenLink = widgets && "onOpenLink" in widgets ? widgets.onOpenLink : openInNewTab;
3848
+ return {
3849
+ ...allow && { allow },
3850
+ ...onOpenLink && { onOpenLink },
3851
+ ...readResource && { readResource }
3852
+ };
3853
+ }, [widgets]);
3854
+ return /* @__PURE__ */ jsxs("div", {
3855
+ "data-chat-density": density,
3856
+ style,
3857
+ className: clsx("chat-view flex h-full min-h-0 flex-col bg-chat-bg text-chat-fg", className),
3858
+ children: [header, /* @__PURE__ */ jsx("div", {
3859
+ className: "chat-view-body flex min-h-0 flex-1 flex-col",
3860
+ children: /* @__PURE__ */ jsx(WidgetHostProvider, {
3861
+ host,
3862
+ children: /* @__PURE__ */ jsx(MessageList, {
3863
+ messages,
3864
+ start,
3865
+ ...notice && { header: notice },
3866
+ ...renderPart && { renderPart },
3867
+ ...footer && { footer: /* @__PURE__ */ jsx("div", {
3868
+ className: "bg-linear-to-t from-chat-bg from-70% to-transparent px-(--chat-gutter) pt-4 pb-[max(1rem,env(safe-area-inset-bottom))]",
3869
+ children: /* @__PURE__ */ jsx("div", {
3870
+ className: "mx-auto w-full max-w-(--chat-content-width)",
3871
+ children: footer
3872
+ })
3873
+ }) }
3874
+ })
3875
+ })
3876
+ })]
3877
+ });
3878
+ }
2706
3879
  // src/views/app-shell.tsx
2707
3880
  /**
2708
3881
  * A page with a sidebar: pinned and collapsible to an icon rail from tablet width up, a drawer over the
@@ -2727,6 +3900,7 @@ function AppShell({ sidebar, children, defaultCollapsed, className }) {
2727
3900
  toggle,
2728
3901
  dismiss
2729
3902
  ]);
3903
+ const labels = useLabels();
2730
3904
  const inDrawer = useMemo(() => ({
2731
3905
  ...pinned,
2732
3906
  collapsed: false
@@ -2745,7 +3919,7 @@ function AppShell({ sidebar, children, defaultCollapsed, className }) {
2745
3919
  }), drawer && /* @__PURE__ */ jsx(Drawer, {
2746
3920
  isOpen: drawerOpen,
2747
3921
  onOpenChange: setDrawerOpen,
2748
- "aria-label": "Sidebar",
3922
+ "aria-label": labels.sidebar,
2749
3923
  children: /* @__PURE__ */ jsx(SidebarContext.Provider, {
2750
3924
  value: inDrawer,
2751
3925
  children: sidebar
@@ -2755,13 +3929,16 @@ function AppShell({ sidebar, children, defaultCollapsed, className }) {
2755
3929
  }
2756
3930
  /** Opens the drawer on phones, collapses / expands the pinned sidebar elsewhere. */
2757
3931
  function SidebarToggle({ drawerOnly = false, className }) {
3932
+ const icons = useIcons();
2758
3933
  const { drawer, drawerOpen, collapsed, toggle } = useSidebar();
3934
+ const labels = useLabels();
2759
3935
  if (drawerOnly && !drawer) return null;
3936
+ const label = drawer ? drawerOpen ? labels.closeSidebar : labels.openSidebar : collapsed ? labels.expandSidebar : labels.collapseSidebar;
2760
3937
  return /* @__PURE__ */ jsx(IconButton, {
2761
- label: drawer ? drawerOpen ? "Close sidebar" : "Open sidebar" : collapsed ? "Expand sidebar" : "Collapse sidebar",
3938
+ label,
2762
3939
  onPress: toggle,
2763
3940
  className,
2764
- children: /* @__PURE__ */ jsx(PanelLeft, {})
3941
+ children: icons.sidebar
2765
3942
  });
2766
3943
  }
2767
3944
  // src/views/sidebar.tsx
@@ -2801,7 +3978,7 @@ function Sidebar({ brand, headerActions, nav, children, footer, className }) {
2801
3978
  /** A nav row: icon and label; just the icon with a tooltip when collapsed. Closes the drawer when picked. */
2802
3979
  function SidebarItem({ icon, label, onPress, href, active, shortcut }) {
2803
3980
  const { collapsed, dismiss } = useSidebar();
2804
- const className = clsx("group flex h-8 cursor-pointer items-center gap-2.5 rounded-chat-sm text-left text-sm 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");
3981
+ 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");
2805
3982
  const press = () => {
2806
3983
  onPress?.();
2807
3984
  dismiss();
@@ -2816,7 +3993,7 @@ function SidebarItem({ icon, label, onPress, href, active, shortcut }) {
2816
3993
  children: label
2817
3994
  }),
2818
3995
  !collapsed && shortcut && /* @__PURE__ */ jsx("kbd", {
2819
- className: "font-chat-sans text-xs text-chat-subtle opacity-0 group-data-hovered:opacity-100",
3996
+ className: "font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle opacity-0 group-data-hovered:opacity-100",
2820
3997
  children: shortcut
2821
3998
  })
2822
3999
  ] });
@@ -2842,6 +4019,7 @@ function SidebarItem({ icon, label, onPress, href, active, shortcut }) {
2842
4019
  const initials = (name) => name.split(/\s+/).slice(0, 2).map((w) => w[0]?.toUpperCase() ?? "").join("");
2843
4020
  /** The account row at the bottom: avatar, name, plan, and a menu. */
2844
4021
  function SidebarUser({ name, detail, avatarUrl, menu }) {
4022
+ const icons = useIcons();
2845
4023
  const { collapsed } = useSidebar();
2846
4024
  const avatar = avatarUrl ? /* @__PURE__ */ jsx("img", {
2847
4025
  src: avatarUrl,
@@ -2849,7 +4027,7 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
2849
4027
  className: "size-8 shrink-0 rounded-full object-cover"
2850
4028
  }) : /* @__PURE__ */ jsx("span", {
2851
4029
  "aria-hidden": true,
2852
- className: "flex size-8 shrink-0 items-center justify-center rounded-full bg-chat-fg text-xs font-medium text-chat-bg",
4030
+ className: "flex size-8 shrink-0 items-center justify-center rounded-full bg-chat-fg text-(length:--chat-ui-font-size-sm) leading-4 font-medium text-chat-bg",
2853
4031
  children: initials(name)
2854
4032
  });
2855
4033
  const button = /* @__PURE__ */ jsxs(Button$1, {
@@ -2858,15 +4036,15 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
2858
4036
  children: [avatar, !collapsed && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
2859
4037
  className: "flex min-w-0 flex-1 flex-col",
2860
4038
  children: [/* @__PURE__ */ jsx("span", {
2861
- className: "truncate text-sm text-chat-fg",
4039
+ className: "truncate text-(length:--chat-ui-font-size) leading-5 text-chat-fg",
2862
4040
  children: name
2863
4041
  }), detail && /* @__PURE__ */ jsx("span", {
2864
- className: "truncate text-xs text-chat-muted",
4042
+ className: "truncate text-(length:--chat-ui-font-size-sm) leading-4 text-chat-muted",
2865
4043
  children: detail
2866
4044
  })]
2867
- }), menu && /* @__PURE__ */ jsx(ChevronsUpDown, {
2868
- "aria-hidden": true,
2869
- className: "size-4 shrink-0 text-chat-muted"
4045
+ }), menu && /* @__PURE__ */ jsx(IconSlot, {
4046
+ className: "text-chat-muted [&_svg]:size-4",
4047
+ children: icons.switcher
2870
4048
  })] })]
2871
4049
  });
2872
4050
  if (!menu) return button;
@@ -2875,4 +4053,4 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
2875
4053
  children: menu
2876
4054
  })] });
2877
4055
  }
2878
- export { ActivityGroup, AgentProvider, AgentRegistry, AppShell, ApprovalCard, AssistantMessage, AttachButton, AttachmentCard, AttachmentRow, Badge, Button, Caret, ChatHeader, ChatList, ChatTheme, ChatTitleMenu, ChatView, CodeBlock, Composer, ContextMenu, ContextPanel, DRAWER_QUERY, Disclosure, Drawer, EmptyState, IconButton, Markdown, Menu, MenuItem, MenuNote, MenuSearch, MenuSection, MenuSeparator, MenuTrigger, MessageList, Modal, ModelPicker, Panel, PastedTextDialog, Popover, Sidebar, SidebarItem, SidebarToggle, SidebarUser, SubmenuTrigger, TokenCounter, ToolViewsProvider, Tooltip, UserMessage, activityHeading, activityLabel, commandTool, defaultToolViews, formatDuration, formatRelativeTime, formatTokens, memoryTool, messageText, sseTransport, useAgentContext, useAgentRegistry, useAgentSnapshot, useAgentTool, useAttachments, useChat, useChatTool, useCountUp, useMediaQuery, useSidebar, useSmoothText, useToolView, useToolViews, userMessage, webSearchTool };
4056
+ export { ActivityGroup, AgentProvider, AgentRegistry, AppShell, ApprovalCard, AssistantMessage, AttachButton, AttachmentCard, AttachmentRow, Badge, Button, Caret, ChatHeader, ChatList, ChatNotice, ChatTheme, ChatTitleMenu, ChatTranscript, ChatView, CodeBlock, Composer, ContextMenu, ContextPanel, DRAWER_QUERY, Disclosure, Drawer, EmptyState, IconButton, Markdown, MarkdownLinksProvider, Menu, MenuItem, MenuNote, MenuSearch, MenuSection, MenuSeparator, MenuTrigger, MessageList, Modal, ModelPicker, Panel, PastedTextDialog, Popover, SegmentedControl, Sidebar, SidebarItem, SidebarToggle, SidebarUser, SubmenuTrigger, TokenCounter, ToolViewsProvider, Tooltip, UiComponentsProvider, UiMarkupBlock, UiTree, UserMessage, WIDGET_MAX_HEIGHT, WidgetFrame, WidgetHostProvider, WidgetPartView, activityHeading, activityLabel, commandTool, defaultIcons, defaultLabels, defaultToolViews, formatDuration, formatRelativeTime, formatTokens, htmlComponent, intersectAllow, memoryTool, messageText, openInNewTab, readTreeInput, readVisualizeInput, sseTransport, uiMarkupToolView, uiTreeToolView, useAgentContext, useAgentRegistry, useAgentSnapshot, useAgentTool, useAttachments, useChat, useChatTool, useCountUp, useIcons, useLabels, useMediaQuery, useSidebar, useSmoothText, useToolView, useToolViews, useUiAction, useUiMarkupLanguage, useUiState, useWidgetHost, userMessage, webFetchTool, webSearchTool, widgetCsp, widgetToolView };