agentic-ui-kit 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  "use client";
2
- import { a as 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-6xbP-myu.js";
3
3
  import { clsx } from "clsx";
4
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";
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
  });
@@ -29,11 +29,11 @@ const variants = {
29
29
  /** Filled with the surface color, e.g. the selected Chat/Cowork toggle. */
30
30
  subtle: "bg-chat-surface text-chat-fg data-hovered:bg-chat-hover data-pressed:bg-chat-selected",
31
31
  /** The send button. */
32
- primary: "bg-chat-accent text-white data-hovered:brightness-110 data-pressed:brightness-95"
32
+ primary: "bg-chat-accent text-chat-accent-fg data-hovered:brightness-110 data-pressed:brightness-95"
33
33
  };
34
34
  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"
35
+ sm: "h-7 gap-1.5 px-2 text-(length:--chat-ui-font-size) leading-5 [@media(pointer:coarse)]:h-9",
36
+ md: "h-8 gap-2 px-2.5 text-(length:--chat-ui-font-size) leading-5 [@media(pointer:coarse)]:h-10"
37
37
  };
38
38
  function Button({ variant = "ghost", size = "md", className, ...props }) {
39
39
  return /* @__PURE__ */ jsx(Button$1, {
@@ -61,7 +61,7 @@ const tones = {
61
61
  };
62
62
  function Badge({ children, tone = "outline", icon, className }) {
63
63
  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),
64
+ 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
65
  children: [icon, children]
66
66
  });
67
67
  }
@@ -75,9 +75,77 @@ function Caret({ className }) {
75
75
  children: /* @__PURE__ */ jsx("path", { d: "M2 1.2 6.2 4 2 6.8z" })
76
76
  });
77
77
  }
78
+ // src/primitives/icons.tsx
79
+ /** Lucide icons, plus the claude.ai caret. Unsized: the places that show them size them with CSS. */
80
+ const defaultIcons = {
81
+ copy: /* @__PURE__ */ jsx(Copy, {}),
82
+ copied: /* @__PURE__ */ jsx(Check, {}),
83
+ retry: /* @__PURE__ */ jsx(RefreshCw, {}),
84
+ edit: /* @__PURE__ */ jsx(Pencil, {}),
85
+ speak: /* @__PURE__ */ jsx(Volume2, {}),
86
+ thumbsUp: /* @__PURE__ */ jsx(ThumbsUp, {}),
87
+ thumbsDown: /* @__PURE__ */ jsx(ThumbsDown, {}),
88
+ send: /* @__PURE__ */ jsx(ArrowUp, {}),
89
+ stop: /* @__PURE__ */ jsx(Square, {}),
90
+ attach: /* @__PURE__ */ jsx(Plus, {}),
91
+ scrollToBottom: /* @__PURE__ */ jsx(ArrowDown, {}),
92
+ tokens: /* @__PURE__ */ jsx(ArrowDown, {}),
93
+ sidebar: /* @__PURE__ */ jsx(PanelLeft, {}),
94
+ switcher: /* @__PURE__ */ jsx(ChevronsUpDown, {}),
95
+ dropdown: /* @__PURE__ */ jsx(ChevronDown, {}),
96
+ expand: /* @__PURE__ */ jsx(ChevronRight, {}),
97
+ caret: /* @__PURE__ */ jsx(Caret, {}),
98
+ submenu: /* @__PURE__ */ jsx(ChevronRight, {}),
99
+ check: /* @__PURE__ */ jsx(Check, {}),
100
+ info: /* @__PURE__ */ jsx(Info, {}),
101
+ warning: /* @__PURE__ */ jsx(AlertTriangle, {}),
102
+ approval: /* @__PURE__ */ jsx(ShieldQuestion, {}),
103
+ web: /* @__PURE__ */ jsx(Globe, {}),
104
+ pin: /* @__PURE__ */ jsx(Pin, {}),
105
+ unpin: /* @__PURE__ */ jsx(PinOff, {}),
106
+ rename: /* @__PURE__ */ jsx(Pencil, {}),
107
+ addToProject: /* @__PURE__ */ jsx(FolderPlus, {}),
108
+ add: /* @__PURE__ */ jsx(Plus, {}),
109
+ share: /* @__PURE__ */ jsx(Share, {}),
110
+ delete: /* @__PURE__ */ jsx(Trash2, {}),
111
+ more: /* @__PURE__ */ jsx(MoreHorizontal, {}),
112
+ folder: /* @__PURE__ */ jsx(Folder, {}),
113
+ folderOpen: /* @__PURE__ */ jsx(FolderOpen, {}),
114
+ file: /* @__PURE__ */ jsx(FileText, {}),
115
+ remove: /* @__PURE__ */ jsx(X, {})
116
+ };
117
+ const IconsContext = createContext(defaultIcons);
118
+ /** Merges `icons` over the icons of the enclosing provider (or the defaults). */
119
+ function IconsProvider({ icons, children }) {
120
+ const parent = useIcons();
121
+ const merged = useMemo(() => icons ? {
122
+ ...parent,
123
+ ...icons
124
+ } : parent, [parent, icons]);
125
+ return /* @__PURE__ */ jsx(IconsContext.Provider, {
126
+ value: merged,
127
+ children
128
+ });
129
+ }
130
+ /** The active icons: the enclosing `ChatTheme`'s `icons` over the defaults. */
131
+ function useIcons() {
132
+ return useContext(IconsContext);
133
+ }
134
+ /**
135
+ * Sizes and colors an icon where no button does it already. Pass the size as `[&_svg]:size-4` so it
136
+ * reaches whatever SVG the host supplies.
137
+ */
138
+ function IconSlot({ children, className }) {
139
+ return /* @__PURE__ */ jsx("span", {
140
+ "aria-hidden": true,
141
+ className: clsx("inline-flex shrink-0 items-center justify-center", className),
142
+ children
143
+ });
144
+ }
78
145
  // src/primitives/disclosure.tsx
79
146
  /** A row that expands to show more, like "Recalled memory ▸". */
80
147
  function Disclosure({ summary, children, className, summaryClassName, panelClassName, indicator = "caret", trailing, ...props }) {
148
+ const icons = useIcons();
81
149
  return /* @__PURE__ */ jsxs(Disclosure$1, {
82
150
  ...props,
83
151
  ...className !== void 0 && { className },
@@ -87,15 +155,18 @@ function Disclosure({ summary, children, className, summaryClassName, panelClass
87
155
  slot: "trigger",
88
156
  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
157
  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"
158
+ indicator === "chevron" && /* @__PURE__ */ jsx(IconSlot, {
159
+ className: "transition-transform group-aria-expanded/trigger:rotate-90 [&_svg]:size-3.5",
160
+ children: icons.expand
93
161
  }),
94
162
  /* @__PURE__ */ jsx("span", {
95
163
  className: "min-w-0 truncate",
96
164
  children: summary
97
165
  }),
98
- indicator === "caret" && /* @__PURE__ */ jsx(Caret, { className: "group-aria-expanded/trigger:rotate-90" }),
166
+ indicator === "caret" && /* @__PURE__ */ jsx(IconSlot, {
167
+ className: "transition-transform duration-150 group-aria-expanded/trigger:rotate-90",
168
+ children: icons.caret
169
+ }),
99
170
  trailing && /* @__PURE__ */ jsx("span", {
100
171
  className: "ml-auto shrink-0 pl-2",
101
172
  children: trailing
@@ -122,7 +193,7 @@ function MenuSearch({ placeholder, children, value, onChange }) {
122
193
  className: "border-b border-chat-popover-border",
123
194
  children: /* @__PURE__ */ jsx(Input, {
124
195
  placeholder,
125
- className: "w-full bg-transparent px-3.5 py-2.5 text-sm text-chat-fg outline-none placeholder:text-chat-subtle"
196
+ 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
197
  })
127
198
  }), children]
128
199
  });
@@ -142,11 +213,12 @@ function Menu({ className, ...props }) {
142
213
  });
143
214
  }
144
215
  function MenuItem({ children, icon, description, detail, shortcut, tone = "default", className, ...props }) {
216
+ const icons = useIcons();
145
217
  const textValue = props.textValue ?? (typeof children === "string" ? children : void 0);
146
218
  return /* @__PURE__ */ jsx(MenuItem$1, {
147
219
  ...props,
148
220
  ...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),
221
+ 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
222
  children: ({ isSelected, hasSubmenu, selectionMode }) => /* @__PURE__ */ jsxs(Fragment, { children: [
151
223
  icon && /* @__PURE__ */ jsx("span", {
152
224
  className: clsx("flex size-4 shrink-0 items-center [&_svg]:size-4", tone === "danger" ? "text-chat-danger" : "text-chat-fg"),
@@ -160,25 +232,25 @@ function MenuItem({ children, icon, description, detail, shortcut, tone = "defau
160
232
  children
161
233
  }), description && /* @__PURE__ */ jsx(Text, {
162
234
  slot: "description",
163
- className: "text-xs text-chat-muted",
235
+ className: "text-(length:--chat-ui-font-size-sm) leading-4 text-chat-muted",
164
236
  children: description
165
237
  })]
166
238
  }),
167
239
  detail && /* @__PURE__ */ jsx("span", {
168
- className: "shrink-0 text-sm text-chat-muted",
240
+ className: "shrink-0 text-(length:--chat-ui-font-size) leading-5 text-chat-muted",
169
241
  children: detail
170
242
  }),
171
243
  shortcut && /* @__PURE__ */ jsx(Keyboard, {
172
- className: "shrink-0 font-chat-sans text-xs text-chat-subtle",
244
+ className: "shrink-0 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle",
173
245
  children: shortcut
174
246
  }),
175
- selectionMode !== "none" && isSelected && /* @__PURE__ */ jsx(Check, {
176
- "aria-hidden": true,
177
- className: "size-4 shrink-0 text-chat-check"
247
+ selectionMode !== "none" && isSelected && /* @__PURE__ */ jsx(IconSlot, {
248
+ className: "text-chat-check [&_svg]:size-4",
249
+ children: icons.check
178
250
  }),
179
- hasSubmenu && /* @__PURE__ */ jsx(ChevronRight, {
180
- "aria-hidden": true,
181
- className: "size-4 shrink-0 text-chat-muted"
251
+ hasSubmenu && /* @__PURE__ */ jsx(IconSlot, {
252
+ className: "text-chat-muted [&_svg]:size-4",
253
+ children: icons.submenu
182
254
  })
183
255
  ] })
184
256
  });
@@ -188,7 +260,7 @@ function MenuSection({ title, children, ...selection }) {
188
260
  ...selection,
189
261
  className: "[&:not(:first-child)]:mt-1",
190
262
  children: [title && /* @__PURE__ */ jsx(Header, {
191
- className: "px-2 pt-1.5 pb-1 text-xs text-chat-subtle",
263
+ className: "px-2 pt-1.5 pb-1 text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle",
192
264
  children: title
193
265
  }), children]
194
266
  });
@@ -199,7 +271,7 @@ function MenuSeparator() {
199
271
  /** Explanatory text inside a menu, above or below the items. Not focusable. */
200
272
  function MenuNote({ children, className }) {
201
273
  return /* @__PURE__ */ jsx("div", {
202
- className: clsx("px-2 py-1.5 text-sm leading-snug text-chat-muted", className),
274
+ className: clsx("px-2 py-1.5 text-(length:--chat-ui-font-size) leading-snug text-chat-muted", className),
203
275
  children
204
276
  });
205
277
  }
@@ -260,7 +332,7 @@ function Modal({ isOpen, onOpenChange, title, children, className, ...props }) {
260
332
  isOpen,
261
333
  onOpenChange,
262
334
  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",
335
+ 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
336
  children: /* @__PURE__ */ jsx(Modal$1, {
265
337
  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
338
  children: /* @__PURE__ */ jsxs(Dialog, {
@@ -281,7 +353,7 @@ function Drawer({ isOpen, onOpenChange, side = "left", children, className, ...p
281
353
  isOpen,
282
354
  onOpenChange,
283
355
  isDismissable: true,
284
- className: "fixed inset-0 z-40 bg-black/50 data-entering:animate-chat-fade-in data-exiting:animate-chat-fade-out",
356
+ className: "fixed inset-0 z-40 bg-(--chat-backdrop) data-entering:animate-chat-fade-in data-exiting:animate-chat-fade-out",
285
357
  children: /* @__PURE__ */ jsx(Modal$1, {
286
358
  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
359
  children: /* @__PURE__ */ jsx(Dialog, {
@@ -303,7 +375,7 @@ function Panel({ title, actions, footer, children, variant = "card", className,
303
375
  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
376
  children: [
305
377
  (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",
378
+ 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
379
  children: [/* @__PURE__ */ jsx("div", {
308
380
  className: "min-w-0 flex-1 truncate font-medium",
309
381
  children: title
@@ -460,10 +532,10 @@ function CodeBlock({ code, language, label = language, variant = "plain", classN
460
532
  style: themeBackground ? result.vars : void 0,
461
533
  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
534
  children: [label && /* @__PURE__ */ jsx("div", {
463
- className: "mb-3 font-chat-sans text-xs text-chat-subtle",
535
+ className: "mb-3 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle",
464
536
  children: label
465
537
  }), /* @__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]"),
538
+ 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
539
  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
540
  className: "chat-code-token",
469
541
  style: token.htmlStyle,
@@ -472,13 +544,130 @@ function CodeBlock({ code, language, label = language, variant = "plain", classN
472
544
  })]
473
545
  });
474
546
  }
547
+ // src/primitives/labels.tsx
548
+ const plural = (count, one, many) => count === 1 ? one : many(count);
549
+ /** The English strings. */
550
+ const defaultLabels = {
551
+ thinking: "Thinking…",
552
+ thought: "Thought",
553
+ thoughtFor: (duration) => `Thought for ${duration}`,
554
+ waitingForYou: " · waiting for you",
555
+ usedTool: (name) => `used ${name}`,
556
+ joinSummaries: (phrases) => {
557
+ const text = phrases.join(", ");
558
+ return text.charAt(0).toUpperCase() + text.slice(1);
559
+ },
560
+ duration: (seconds) => seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`,
561
+ ranCommandLabel: "Ran a command",
562
+ ranCommands: (count) => plural(count, "ran a command", (n) => `ran ${n} commands`),
563
+ searchedWebLabel: "Searched the web",
564
+ readPageLabel: "Read page",
565
+ readPages: (count) => plural(count, "read a page", (n) => `read ${n} pages`),
566
+ searchedWeb: (count) => plural(count, "searched the web", (n) => `searched the web ${n} times`),
567
+ recalledMemoryLabel: "Recalled memory",
568
+ recalledMemory: () => "recalled memory",
569
+ output: "Output",
570
+ noOutput: "(no output)",
571
+ noResults: "No results",
572
+ reasoningHidden: "The model's reasoning is not shown.",
573
+ allowTool: (name) => `Allow ${name}?`,
574
+ allowAction: (label) => /* @__PURE__ */ jsxs(Fragment, { children: [
575
+ "Allow ",
576
+ label,
577
+ "?"
578
+ ] }),
579
+ allow: "Allow",
580
+ deny: "Deny",
581
+ sidebar: "Sidebar",
582
+ closeSidebar: "Close sidebar",
583
+ openSidebar: "Open sidebar",
584
+ expandSidebar: "Expand sidebar",
585
+ collapseSidebar: "Collapse sidebar",
586
+ context: "Context",
587
+ contextUsed: "Context used",
588
+ percentUsed: (percent) => `${percent}% used`,
589
+ freeSpace: "Free space",
590
+ tokenCount: (count) => `${count} tokens`,
591
+ replyPlaceholder: "Reply…",
592
+ replyDirectly: "Or reply directly…",
593
+ message: "Message",
594
+ stop: "Stop",
595
+ send: "Send",
596
+ addFiles: "Add files and more",
597
+ attachments: "Attachments",
598
+ editPastedText: "Edit pasted text",
599
+ viewPastedText: "View pasted text",
600
+ removeAttachment: (name) => `Remove ${name}`,
601
+ pastedText: "Pasted text",
602
+ pasted: "Pasted",
603
+ failed: "Failed",
604
+ file: "File",
605
+ uploadFailed: "Upload failed",
606
+ cancel: "Cancel",
607
+ close: "Close",
608
+ save: "Save",
609
+ chats: "Chats",
610
+ running: "Running",
611
+ unread: "Unread",
612
+ moreOptionsFor: (title) => `More options for ${title}`,
613
+ new: "New",
614
+ viewAll: "View all",
615
+ chatActions: "Chat actions",
616
+ pin: "Pin",
617
+ unpin: "Unpin",
618
+ rename: "Rename",
619
+ addToProject: "Add to project",
620
+ searchProjects: "Search or create a project",
621
+ projects: "Projects",
622
+ newProject: "New project",
623
+ shareChat: "Share chat",
624
+ delete: "Delete",
625
+ justNow: "just now",
626
+ copy: "Copy",
627
+ copied: "Copied",
628
+ retry: "Retry",
629
+ edit: "Edit",
630
+ readAloud: "Read aloud",
631
+ goodResponse: "Good response",
632
+ badResponse: "Bad response",
633
+ somethingWentWrong: "Something went wrong.",
634
+ conversation: "Conversation",
635
+ scrollToBottom: "Scroll to bottom",
636
+ showMore: "Show more",
637
+ showLess: "Show less",
638
+ model: "Model",
639
+ modelButton: (label) => `Model: ${label}`,
640
+ effort: "Effort",
641
+ effortNote: "Higher effort means more thorough responses, but takes longer and uses your limits faster.",
642
+ recommended: "Recommended",
643
+ moreModels: "More models",
644
+ widget: "Widget",
645
+ drawingWidget: "Drawing"
646
+ };
647
+ const LabelsContext = createContext(defaultLabels);
648
+ /** Merges `labels` over the labels of the enclosing provider (or the defaults). */
649
+ function LabelsProvider({ labels, children }) {
650
+ const parent = useLabels();
651
+ const merged = useMemo(() => labels ? {
652
+ ...parent,
653
+ ...labels
654
+ } : parent, [parent, labels]);
655
+ return /* @__PURE__ */ jsx(LabelsContext.Provider, {
656
+ value: merged,
657
+ children
658
+ });
659
+ }
660
+ /** The active UI strings: the enclosing `ChatTheme`'s `labels` over the English defaults. */
661
+ function useLabels() {
662
+ return useContext(LabelsContext);
663
+ }
475
664
  // src/primitives/chat-theme.tsx
476
665
  /**
477
666
  * Applies a chat theme to part of a page. Menus, tooltips and dialogs opened inside render into this
478
667
  * element instead of `<body>`, so they get the same theme. An app themed as a whole (variables on
479
668
  * `:root`) does not need it for colors, but still uses it for `codeThemes` and `motion`.
480
669
  */
481
- function ChatTheme({ theme, codeThemes, motion, children, className }) {
670
+ function ChatTheme({ theme, codeThemes, motion, labels, icons, children, className }) {
482
671
  const portal = useRef(null);
483
672
  const getContainer = useCallback(() => portal.current, []);
484
673
  return /* @__PURE__ */ jsxs("div", {
@@ -488,9 +677,15 @@ function ChatTheme({ theme, codeThemes, motion, children, className }) {
488
677
  children: [/* @__PURE__ */ jsx(AppearanceProvider, {
489
678
  codeThemes,
490
679
  motion,
491
- children: /* @__PURE__ */ jsx(UNSAFE_PortalProvider, {
492
- getContainer,
493
- children
680
+ children: /* @__PURE__ */ jsx(LabelsProvider, {
681
+ labels,
682
+ children: /* @__PURE__ */ jsx(IconsProvider, {
683
+ icons,
684
+ children: /* @__PURE__ */ jsx(UNSAFE_PortalProvider, {
685
+ getContainer,
686
+ children
687
+ })
688
+ })
494
689
  })
495
690
  }), /* @__PURE__ */ jsx("div", { ref: portal })]
496
691
  });
@@ -1012,6 +1207,700 @@ const SidebarContext = createContext({
1012
1207
  dismiss: () => {}
1013
1208
  });
1014
1209
  const useSidebar = () => useContext(SidebarContext);
1210
+ // src/components/widget-frame.tsx
1211
+ const WidgetHostContext = createContext({});
1212
+ /** Sets what widgets below may do. ChatView and ChatTranscript set it from their `widgets` prop. */
1213
+ function WidgetHostProvider({ host, children }) {
1214
+ return /* @__PURE__ */ jsx(WidgetHostContext.Provider, {
1215
+ value: host,
1216
+ children
1217
+ });
1218
+ }
1219
+ function useWidgetHost() {
1220
+ return useContext(WidgetHostContext);
1221
+ }
1222
+ /** The tallest a widget may grow, in px. */
1223
+ const WIDGET_MAX_HEIGHT = 2e3;
1224
+ const PROTOCOL_VERSION = "2026-01-26";
1225
+ const THEME_VARS = [
1226
+ "bg",
1227
+ "fg",
1228
+ "muted",
1229
+ "subtle",
1230
+ "surface",
1231
+ "raised",
1232
+ "hover",
1233
+ "selected",
1234
+ "border",
1235
+ "border-strong",
1236
+ "divider",
1237
+ "accent",
1238
+ "accent-fg",
1239
+ "check",
1240
+ "warning",
1241
+ "danger",
1242
+ "code-bg",
1243
+ "font-sans",
1244
+ "font-serif",
1245
+ "font-mono",
1246
+ "radius-sm",
1247
+ "radius-md",
1248
+ "radius-lg",
1249
+ "ui-font-size",
1250
+ "ui-font-size-sm",
1251
+ "message-font-size"
1252
+ ];
1253
+ const ORIGIN = /^(https?|wss?):\/\/(\*\.)?[a-z0-9-]+(\.[a-z0-9-]+)*(:\d{1,5})?$/;
1254
+ function origins(list) {
1255
+ return (list ?? []).map((d) => d.trim().toLowerCase().replace(/\/$/, "")).filter((d) => ORIGIN.test(d));
1256
+ }
1257
+ /** Whether `origin` is covered by `allowed`, which may be a wildcard like "https://*.example.com". */
1258
+ function covers(allowed, origin) {
1259
+ if (allowed === origin) return true;
1260
+ const wild = /^([a-z]+:\/\/)\*\.(.+)$/.exec(allowed);
1261
+ return !!wild && origin.startsWith(wild[1]) && origin.slice(wild[1].length).endsWith("." + wild[2]) && !origin.includes("*");
1262
+ }
1263
+ /** The part of a widget's request that the host allows. */
1264
+ function intersectAllow(request, allow = {}) {
1265
+ const keep = (asked, max) => origins(asked).filter((o) => origins(max).some((m) => covers(m, o)));
1266
+ return {
1267
+ resourceDomains: keep(request.resourceDomains, allow.resourceDomains),
1268
+ connectDomains: keep(request.connectDomains, allow.connectDomains)
1269
+ };
1270
+ }
1271
+ /** The Content-Security-Policy of a widget's document. */
1272
+ function widgetCsp(allow = {}) {
1273
+ const res = origins(allow.resourceDomains).join(" ");
1274
+ const conn = origins(allow.connectDomains).join(" ");
1275
+ const with_ = (base) => res ? `${base} ${res}` : base;
1276
+ return [
1277
+ "default-src 'none'",
1278
+ with_("script-src 'unsafe-inline'"),
1279
+ with_("style-src 'unsafe-inline'"),
1280
+ with_("img-src data: blob:"),
1281
+ with_("font-src data:"),
1282
+ with_("media-src data: blob:"),
1283
+ `connect-src ${conn || "'none'"}`,
1284
+ "frame-src 'none'",
1285
+ "base-uri 'none'",
1286
+ "form-action 'none'"
1287
+ ].join("; ");
1288
+ }
1289
+ /** Runs inside a fragment's frame: renders the markup, reports its height, offers sendPrompt and openLink. */
1290
+ const BOOT = `(() => {
1291
+ const root = document.getElementById("widget");
1292
+ let id = 0;
1293
+ const post = (method, params, request) => parent.postMessage(request ? { jsonrpc: "2.0", id: ++id, method, params } : { jsonrpc: "2.0", method, params }, "*");
1294
+ window.sendPrompt = (text) => post("ui/message", { role: "user", content: [{ type: "text", text: String(text) }] }, true);
1295
+ window.openLink = (url) => post("ui/open-link", { url: String(url) }, true);
1296
+ let done = false;
1297
+ const runScripts = async () => {
1298
+ for (const old of [...root.querySelectorAll("script")]) {
1299
+ const s = document.createElement("script");
1300
+ for (const a of old.attributes) s.setAttribute(a.name, a.value);
1301
+ s.textContent = old.textContent;
1302
+ const loaded = s.src ? new Promise((ok) => { s.onload = s.onerror = ok; }) : null;
1303
+ old.replaceWith(s);
1304
+ if (loaded) await loaded;
1305
+ }
1306
+ };
1307
+ const theme = (ctx) => {
1308
+ const vars = (ctx && ctx.styles && ctx.styles.variables) || {};
1309
+ for (const k in vars) if (k.startsWith("--chat-")) document.documentElement.style.setProperty(k, vars[k]);
1310
+ if (ctx && ctx.theme) document.documentElement.style.colorScheme = ctx.theme;
1311
+ };
1312
+ addEventListener("message", (e) => {
1313
+ if (e.source !== parent) return;
1314
+ const m = e.data || {};
1315
+ if (m.method === "ui/notifications/host-context-changed") return theme(m.params);
1316
+ const code = m.params && m.params.arguments && m.params.arguments.widget_code;
1317
+ if (typeof code !== "string" || done) return;
1318
+ if (m.method === "ui/notifications/tool-input-partial") root.innerHTML = code;
1319
+ if (m.method === "ui/notifications/tool-input") { done = true; root.innerHTML = code; runScripts(); }
1320
+ });
1321
+ let last = -1;
1322
+ new ResizeObserver(() => {
1323
+ const h = Math.ceil(root.getBoundingClientRect().height);
1324
+ if (h !== last) { last = h; post("ui/notifications/size-changed", { height: h }); }
1325
+ }).observe(root);
1326
+ const seen = new Set();
1327
+ const report = (text) => { if (!seen.has(text) && seen.size < 10) { seen.add(text); post("notifications/message", { level: "error", logger: "widget", data: text }); } };
1328
+ addEventListener("error", (e) => report(String(e.message) + (e.lineno ? " (line " + e.lineno + ")" : "")));
1329
+ addEventListener("unhandledrejection", (e) => report("Unhandled promise rejection: " + String(e.reason && e.reason.message || e.reason)));
1330
+ post("ui/notifications/initialized", {});
1331
+ })();`;
1332
+ function fragmentDoc(csp, theme) {
1333
+ return `<!doctype html><html><head><meta charset="utf-8">
1334
+ <meta http-equiv="Content-Security-Policy" content="${csp}">
1335
+ <style>
1336
+ :root { ${Object.entries(theme.vars).map(([k, v]) => `${k}: ${v};`).join(" ")} color-scheme: ${theme.scheme}; }
1337
+ html, body { margin: 0; background: transparent; color: var(--chat-fg); font-family: var(--chat-font-sans);
1338
+ font-size: var(--chat-ui-font-size); line-height: 1.5; }
1339
+ #widget { display: flow-root; }
1340
+ *, *::before, *::after { box-sizing: border-box; }
1341
+ button, input, select, textarea { font: inherit; color: inherit; }
1342
+ </style></head><body><div id="widget"></div><script>${BOOT}<\/script></body></html>`;
1343
+ }
1344
+ /** An app's own document with the host's CSP put first in its head (a second policy can only narrow it). */
1345
+ function appDoc(csp, html) {
1346
+ const meta = `<meta http-equiv="Content-Security-Policy" content="${csp}">`;
1347
+ const head = /<head(\s[^>]*)?>/i.exec(html);
1348
+ if (head) return html.slice(0, head.index + head[0].length) + meta + html.slice(head.index + head[0].length);
1349
+ const root = /<html(\s[^>]*)?>/i.exec(html);
1350
+ if (root) return html.slice(0, root.index + root[0].length) + `<head>${meta}</head>` + html.slice(root.index + root[0].length);
1351
+ return `<!doctype html><html><head>${meta}</head><body>${html}</body></html>`;
1352
+ }
1353
+ function readTheme(el) {
1354
+ const style = getComputedStyle(el);
1355
+ return {
1356
+ vars: Object.fromEntries(THEME_VARS.map((v) => [`--chat-${v}`, style.getPropertyValue(`--chat-${v}`).trim()])),
1357
+ scheme: /light/.test(style.colorScheme) && !/dark/.test(style.colorScheme) ? "light" : "dark"
1358
+ };
1359
+ }
1360
+ /** Calls `onChange` when anything above `el` changes attributes (a theme switch) or the OS scheme flips. */
1361
+ function watchTheme(el, onChange) {
1362
+ const observer = new MutationObserver(onChange);
1363
+ for (let node = el; node; node = node.parentElement) observer.observe(node, {
1364
+ attributes: true,
1365
+ attributeFilter: [
1366
+ "class",
1367
+ "style",
1368
+ "data-chat-theme"
1369
+ ]
1370
+ });
1371
+ const media = typeof matchMedia === "function" ? matchMedia("(prefers-color-scheme: dark)") : void 0;
1372
+ media?.addEventListener?.("change", onChange);
1373
+ return () => {
1374
+ observer.disconnect();
1375
+ media?.removeEventListener?.("change", onChange);
1376
+ };
1377
+ }
1378
+ function WidgetFrame({ html, document: isDocument = false, streaming = false, title, loadingMessages, allow, csp: request, onPrompt, onOpenLink, onCallTool, onError, toolInput, toolResult, cancelled, className }) {
1379
+ const labels = useLabels();
1380
+ const frame = useRef(null);
1381
+ const host = useRef(null);
1382
+ const [ready, setReady] = useState(false);
1383
+ const [height, setHeight] = useState(0);
1384
+ const [initial, setInitial] = useState();
1385
+ const theme = useRef(void 0);
1386
+ const [tick, setTick] = useState(0);
1387
+ const effective = request ? intersectAllow(request, allow) : isDocument ? {} : allow ?? {};
1388
+ const policy = widgetCsp(effective);
1389
+ const doc = useMemo(() => initial && (isDocument ? appDoc(policy, html) : fragmentDoc(policy, initial)), [
1390
+ initial,
1391
+ policy,
1392
+ isDocument,
1393
+ isDocument && html
1394
+ ]);
1395
+ const latest = useRef({
1396
+ onPrompt,
1397
+ onOpenLink,
1398
+ onCallTool,
1399
+ onError,
1400
+ policy,
1401
+ effective
1402
+ });
1403
+ latest.current = {
1404
+ onPrompt,
1405
+ onOpenLink,
1406
+ onCallTool,
1407
+ onError,
1408
+ policy,
1409
+ effective
1410
+ };
1411
+ const send = (message) => frame.current?.contentWindow?.postMessage({
1412
+ jsonrpc: "2.0",
1413
+ ...message
1414
+ }, "*");
1415
+ const context = () => ({
1416
+ theme: theme.current?.scheme,
1417
+ styles: { variables: theme.current?.vars },
1418
+ displayMode: "inline",
1419
+ availableDisplayModes: ["inline"],
1420
+ containerDimensions: {
1421
+ maxHeight: WIDGET_MAX_HEIGHT,
1422
+ ...host.current && { width: host.current.clientWidth }
1423
+ },
1424
+ platform: "web",
1425
+ ...labels.locale && { locale: labels.locale }
1426
+ });
1427
+ useEffect(() => {
1428
+ const el = host.current;
1429
+ theme.current = readTheme(el);
1430
+ setInitial(theme.current);
1431
+ return watchTheme(el, () => {
1432
+ const next = readTheme(el);
1433
+ if (JSON.stringify(next) === JSON.stringify(theme.current)) return;
1434
+ theme.current = next;
1435
+ send({
1436
+ method: "ui/notifications/host-context-changed",
1437
+ params: {
1438
+ theme: next.scheme,
1439
+ styles: { variables: next.vars }
1440
+ }
1441
+ });
1442
+ });
1443
+ }, []);
1444
+ useEffect(() => setReady(false), [doc]);
1445
+ useEffect(() => {
1446
+ const reply = (id, result) => id !== void 0 && send({
1447
+ id,
1448
+ result
1449
+ });
1450
+ const fail = (id, code, message) => id !== void 0 && send({
1451
+ id,
1452
+ error: {
1453
+ code,
1454
+ message
1455
+ }
1456
+ });
1457
+ const onMessage = async (e) => {
1458
+ if (!frame.current || e.source !== frame.current.contentWindow) return;
1459
+ const m = e.data ?? {};
1460
+ if (typeof m !== "object" || typeof m.method !== "string") return;
1461
+ const { onPrompt, onOpenLink, onCallTool, effective } = latest.current;
1462
+ switch (m.method) {
1463
+ case "ui/initialize": return reply(m.id, {
1464
+ protocolVersion: PROTOCOL_VERSION,
1465
+ hostInfo: {
1466
+ name: "agentic-ui-kit",
1467
+ version: "1"
1468
+ },
1469
+ hostCapabilities: {
1470
+ ...onOpenLink && { openLinks: {} },
1471
+ ...onCallTool && { serverTools: {} },
1472
+ logging: {},
1473
+ sandbox: { csp: effective }
1474
+ },
1475
+ hostContext: context()
1476
+ });
1477
+ case "ui/notifications/initialized": return setReady(true);
1478
+ case "ui/notifications/size-changed": return setHeight(Math.min(Math.max(0, Number(m.params?.height) || 0), WIDGET_MAX_HEIGHT));
1479
+ case "ui/message": {
1480
+ const text = (Array.isArray(m.params?.content) ? m.params.content : []).find((c) => c?.type === "text")?.text;
1481
+ if (!onPrompt) return fail(m.id, -32e3, "Sending messages is off");
1482
+ if (typeof text !== "string" || !text.trim()) return fail(m.id, -32602, "Empty message");
1483
+ onPrompt(text.slice(0, 4e3));
1484
+ return reply(m.id, {});
1485
+ }
1486
+ case "ui/open-link": {
1487
+ const url = String(m.params?.url ?? "");
1488
+ if (!onOpenLink) return fail(m.id, -32e3, "Opening links is off");
1489
+ if (!/^https?:\/\//i.test(url)) return fail(m.id, -32602, "Only http(s) links");
1490
+ onOpenLink(url);
1491
+ return reply(m.id, {});
1492
+ }
1493
+ case "tools/call": {
1494
+ if (!onCallTool) return fail(m.id, -32e3, "Calling tools is off");
1495
+ const name = String(m.params?.name ?? "");
1496
+ const args = m.params?.arguments ?? {};
1497
+ try {
1498
+ return reply(m.id, await onCallTool(name, args));
1499
+ } catch (error) {
1500
+ return fail(m.id, -32e3, error instanceof Error ? error.message : String(error));
1501
+ }
1502
+ }
1503
+ case "ping": return reply(m.id, {});
1504
+ case "notifications/message": {
1505
+ const data = typeof m.params?.data === "string" ? m.params.data : JSON.stringify(m.params?.data);
1506
+ if (m.params?.level === "error" && data) latest.current.onError?.(data.slice(0, 1e3));
1507
+ return;
1508
+ }
1509
+ default: return fail(m.id, -32601, "Method not found");
1510
+ }
1511
+ };
1512
+ window.addEventListener("message", onMessage);
1513
+ return () => window.removeEventListener("message", onMessage);
1514
+ }, []);
1515
+ useEffect(() => {
1516
+ if (!ready || isDocument) return;
1517
+ send({
1518
+ method: streaming ? "ui/notifications/tool-input-partial" : "ui/notifications/tool-input",
1519
+ params: { arguments: { widget_code: html } }
1520
+ });
1521
+ }, [
1522
+ ready,
1523
+ isDocument,
1524
+ html,
1525
+ streaming
1526
+ ]);
1527
+ const inputKey = isDocument && toolInput !== void 0 ? JSON.stringify(toolInput) : void 0;
1528
+ useEffect(() => {
1529
+ if (!ready || inputKey === void 0) return;
1530
+ send({
1531
+ method: streaming ? "ui/notifications/tool-input-partial" : "ui/notifications/tool-input",
1532
+ params: { arguments: JSON.parse(inputKey) }
1533
+ });
1534
+ }, [
1535
+ ready,
1536
+ inputKey,
1537
+ streaming
1538
+ ]);
1539
+ useEffect(() => {
1540
+ if (ready && isDocument && toolResult !== void 0) send({
1541
+ method: "ui/notifications/tool-result",
1542
+ params: toolResult
1543
+ });
1544
+ }, [
1545
+ ready,
1546
+ isDocument,
1547
+ toolResult
1548
+ ]);
1549
+ useEffect(() => {
1550
+ if (ready && isDocument && cancelled !== void 0) send({
1551
+ method: "ui/notifications/tool-cancelled",
1552
+ params: { reason: cancelled }
1553
+ });
1554
+ }, [
1555
+ ready,
1556
+ isDocument,
1557
+ cancelled
1558
+ ]);
1559
+ const lines = loadingMessages?.length ? loadingMessages : [labels.drawingWidget];
1560
+ useEffect(() => {
1561
+ if (!streaming || lines.length < 2) return;
1562
+ const id = setInterval(() => setTick((t) => t + 1), 1800);
1563
+ return () => clearInterval(id);
1564
+ }, [streaming, lines.length]);
1565
+ return /* @__PURE__ */ jsxs("figure", {
1566
+ ref: host,
1567
+ className: className ?? "my-2",
1568
+ "aria-label": title ?? labels.widget,
1569
+ "aria-busy": streaming || void 0,
1570
+ children: [streaming && /* @__PURE__ */ jsx("div", {
1571
+ className: "mb-2 animate-pulse font-chat-sans text-(length:--chat-ui-font-size) text-chat-muted motion-reduce:animate-none",
1572
+ children: lines[tick % lines.length]
1573
+ }), doc && /* @__PURE__ */ jsx("iframe", {
1574
+ ref: frame,
1575
+ title: title ?? labels.widget,
1576
+ sandbox: "allow-scripts",
1577
+ srcDoc: doc,
1578
+ style: {
1579
+ height: height || (streaming ? 120 : 0),
1580
+ transition: "height 150ms ease-out"
1581
+ },
1582
+ className: "block w-full border-0 bg-transparent"
1583
+ })]
1584
+ });
1585
+ }
1586
+ /** Pulls the host's abilities from context into frame props. */
1587
+ function useHostProps(partId) {
1588
+ const { allow, onPrompt, onOpenLink, onCallTool, onWidgetError } = useWidgetHost();
1589
+ return {
1590
+ ...allow && { allow },
1591
+ ...onPrompt && { onPrompt },
1592
+ ...onOpenLink && { onOpenLink },
1593
+ ...onCallTool && { onCallTool },
1594
+ ...onWidgetError && { onError: (message) => onWidgetError({
1595
+ partId,
1596
+ message
1597
+ }) }
1598
+ };
1599
+ }
1600
+ /** The default look of a `widget` part: inline html, or an app UI the host reads from `resourceUri`. */
1601
+ function WidgetPartView({ part }) {
1602
+ const host = useWidgetHost();
1603
+ const props = useHostProps(part.id);
1604
+ const [resource, setResource] = useState();
1605
+ const uri = part.html === void 0 ? part.resourceUri : void 0;
1606
+ useEffect(() => {
1607
+ if (!uri || !host.readResource) return;
1608
+ let live = true;
1609
+ host.readResource(uri).then((r) => live && setResource(r), (e) => console.warn("widget:", e));
1610
+ return () => {
1611
+ live = false;
1612
+ };
1613
+ }, [uri, host.readResource]);
1614
+ if (part.html !== void 0) return /* @__PURE__ */ jsx(WidgetFrame, {
1615
+ html: part.html,
1616
+ streaming: part.status === "streaming",
1617
+ ...part.title && { title: part.title },
1618
+ ...props
1619
+ });
1620
+ if (!resource) return null;
1621
+ return /* @__PURE__ */ jsx(WidgetFrame, {
1622
+ document: true,
1623
+ html: resource.html,
1624
+ ...resource.csp && { csp: resource.csp },
1625
+ ...part.title && { title: part.title },
1626
+ ...props
1627
+ });
1628
+ }
1629
+ const str = (v) => typeof v === "string" ? v : void 0;
1630
+ /** Reads the fields of the suggested `visualizeTool` (widget_code, title, loading_messages). */
1631
+ function readVisualizeInput(input) {
1632
+ const i = input ?? {};
1633
+ const loading = Array.isArray(i.loading_messages) ? i.loading_messages.filter((l) => typeof l === "string" && l !== "") : [];
1634
+ return {
1635
+ ...str(i.widget_code) !== void 0 && { html: str(i.widget_code) },
1636
+ ...str(i.title) && { title: str(i.title) },
1637
+ ...loading.length && { loadingMessages: loading.slice(0, 4) }
1638
+ };
1639
+ }
1640
+ /**
1641
+ * A tool view that draws a call's (streaming) input in a WidgetFrame, for whatever tool your harness uses
1642
+ * to write HTML. Register it under your tool's name: `{ show_widget: widgetToolView() }`. `read` maps your
1643
+ * tool's input (possibly cut short while it streams) to the frame's fields; the default reads the suggested
1644
+ * `visualizeTool`'s fields.
1645
+ */
1646
+ function widgetToolView(read = readVisualizeInput) {
1647
+ function WidgetToolBlock({ part }) {
1648
+ const props = useHostProps(part.id);
1649
+ const { html = "", title, loadingMessages } = read(part.input ?? part.partialInput);
1650
+ if (part.status === "error" && !html) return null;
1651
+ return /* @__PURE__ */ jsx(WidgetFrame, {
1652
+ html,
1653
+ streaming: part.status === "streaming" || part.status === "input",
1654
+ ...title && { title },
1655
+ ...loadingMessages?.length && { loadingMessages },
1656
+ ...props
1657
+ });
1658
+ }
1659
+ return { Block: WidgetToolBlock };
1660
+ }
1661
+ /** The default `onOpenLink` of ChatView and ChatTranscript: a new tab with no opener. */
1662
+ function openInNewTab(url) {
1663
+ window.open(url, "_blank", "noopener,noreferrer");
1664
+ }
1665
+ // src/components/ui-tree.tsx
1666
+ const RegistryContext = createContext(void 0);
1667
+ /**
1668
+ * The components the model may use: in ```ui blocks of its replies (see `uiMarkupGuide` in
1669
+ * agentic-ui-kit/core) and in tool calls drawn with `uiTreeToolView`. `persist` keeps each widget's state.
1670
+ */
1671
+ function UiComponentsProvider({ components, persist, markup = "ui", children }) {
1672
+ const value = useMemo(() => ({
1673
+ components: new Map(components.map((c) => [c.name, c])),
1674
+ persist,
1675
+ markup
1676
+ }), [
1677
+ components,
1678
+ persist,
1679
+ markup
1680
+ ]);
1681
+ return /* @__PURE__ */ jsx(RegistryContext.Provider, {
1682
+ value,
1683
+ children
1684
+ });
1685
+ }
1686
+ const TreeContext = createContext({
1687
+ state: {},
1688
+ run: () => {},
1689
+ streaming: false
1690
+ });
1691
+ /** Inside a registered component: runs an action from its props, e.g. `run(props.onPress)`. Bad actions do nothing. */
1692
+ function useUiAction() {
1693
+ const { run } = useContext(TreeContext);
1694
+ return useCallback((action) => matchesSchema(uiActionSchema, action) && run(action), [run]);
1695
+ }
1696
+ /** Inside a registered component: the widget's state and a setter, for inputs that write to it. */
1697
+ function useUiState() {
1698
+ const { state, run } = useContext(TreeContext);
1699
+ return [state, useCallback((key, value) => run({
1700
+ action: "set",
1701
+ key,
1702
+ value
1703
+ }), [run])];
1704
+ }
1705
+ /** Replaces `{ "$state": key }` anywhere in the props with the state's value. */
1706
+ function resolve(value, state) {
1707
+ if (isStateRef(value)) return state[value.$state];
1708
+ if (Array.isArray(value)) return value.map((v) => resolve(v, state));
1709
+ if (typeof value === "object" && value !== null) {
1710
+ const out = {};
1711
+ for (const [k, v] of Object.entries(value)) {
1712
+ const r = resolve(v, state);
1713
+ if (r !== void 0) out[k] = r;
1714
+ }
1715
+ return out;
1716
+ }
1717
+ return value;
1718
+ }
1719
+ /** A component that throws renders nothing instead of taking the message down. */
1720
+ var Contain = class extends Component {
1721
+ state = { failed: false };
1722
+ static getDerivedStateFromError() {
1723
+ return { failed: true };
1724
+ }
1725
+ componentDidCatch(error) {
1726
+ console.warn("render_ui:", error);
1727
+ }
1728
+ render() {
1729
+ return this.state.failed ? null : this.props.children;
1730
+ }
1731
+ };
1732
+ function Node({ node, registry }) {
1733
+ const { state } = useContext(TreeContext);
1734
+ if (Array.isArray(node)) return node.map((child, i) => /* @__PURE__ */ jsx(Node, {
1735
+ node: child,
1736
+ registry
1737
+ }, i));
1738
+ if (typeof node !== "object" || node === null) return null;
1739
+ const { component, props = {}, children } = node;
1740
+ const spec = typeof component === "string" ? registry.components.get(component) : void 0;
1741
+ if (!spec || typeof props !== "object") return null;
1742
+ const resolved = resolve(props, state);
1743
+ if (!matchesSchema(spec.props ?? { type: "object" }, resolved)) return null;
1744
+ const Render = spec.render;
1745
+ const kids = spec.children && Array.isArray(children) ? children.map((child, i) => /* @__PURE__ */ jsx(Node, {
1746
+ node: child,
1747
+ registry
1748
+ }, i)) : void 0;
1749
+ return /* @__PURE__ */ jsx(Contain, { children: /* @__PURE__ */ jsx(Render, {
1750
+ ...resolved,
1751
+ children: kids
1752
+ }) });
1753
+ }
1754
+ /** Draws a `render_ui` tree with the registered components. Unknown components and bad props render nothing. */
1755
+ function UiTree({ id, root, state: initial, streaming = false }) {
1756
+ const registry = useContext(RegistryContext);
1757
+ const host = useWidgetHost();
1758
+ const [local, setLocal] = useState(() => registry?.persist?.load(id) ?? {});
1759
+ const changed = useRef(false);
1760
+ const latest = useRef(host);
1761
+ latest.current = host;
1762
+ useEffect(() => {
1763
+ if (changed.current) registry?.persist?.save(id, local);
1764
+ }, [
1765
+ id,
1766
+ local,
1767
+ registry
1768
+ ]);
1769
+ const run = useCallback((action) => {
1770
+ switch (action.action) {
1771
+ case "set":
1772
+ changed.current = true;
1773
+ return setLocal((s) => ({
1774
+ ...s,
1775
+ [action.key]: action.value
1776
+ }));
1777
+ case "send": return latest.current.onPrompt?.(action.text);
1778
+ case "open": return /^https?:\/\//i.test(action.url) && latest.current.onOpenLink?.(action.url);
1779
+ case "copy":
1780
+ navigator.clipboard?.writeText(action.text).catch(() => {});
1781
+ return;
1782
+ }
1783
+ }, []);
1784
+ const state = useMemo(() => ({
1785
+ ...typeof initial === "object" && initial ? initial : {},
1786
+ ...local
1787
+ }), [initial, local]);
1788
+ const context = useMemo(() => ({
1789
+ state,
1790
+ run,
1791
+ streaming
1792
+ }), [
1793
+ state,
1794
+ run,
1795
+ streaming
1796
+ ]);
1797
+ if (!registry) return null;
1798
+ return /* @__PURE__ */ jsx(TreeContext.Provider, {
1799
+ value: context,
1800
+ children: /* @__PURE__ */ jsx("div", {
1801
+ className: "chat-ui-tree my-2",
1802
+ "aria-busy": streaming || void 0,
1803
+ children: /* @__PURE__ */ jsx(Node, {
1804
+ node: root,
1805
+ registry
1806
+ })
1807
+ })
1808
+ });
1809
+ }
1810
+ /** The fence language drawn as UI here, or undefined outside a `UiComponentsProvider`. */
1811
+ function useUiMarkupLanguage() {
1812
+ const registry = useContext(RegistryContext);
1813
+ return registry && registry.markup !== false ? registry.markup : void 0;
1814
+ }
1815
+ /** A ```ui block from a reply, drawn with the registered components. Safe on a block that is still streaming. */
1816
+ function UiMarkupBlock({ id, source, streaming = false }) {
1817
+ const { nodes, state } = useMemo(() => parseUiMarkup(source), [source]);
1818
+ return /* @__PURE__ */ jsx(UiTree, {
1819
+ id,
1820
+ root: nodes,
1821
+ state,
1822
+ streaming
1823
+ });
1824
+ }
1825
+ /**
1826
+ * A tool view that draws a call's (streaming) JSX markup with your registered components, for whatever tool
1827
+ * your harness uses; the suggested one is core's `showUiTool`. Register it under your tool's name:
1828
+ * `{ show_ui: uiMarkupToolView() }`. `read` maps your tool's input to the markup; the default reads `markup`.
1829
+ */
1830
+ function uiMarkupToolView(read = (input) => {
1831
+ const markup = input?.markup;
1832
+ return typeof markup === "string" ? markup : void 0;
1833
+ }) {
1834
+ function UiMarkupToolBlock({ part }) {
1835
+ const markup = read(part.input ?? part.partialInput);
1836
+ if (!markup) return null;
1837
+ return /* @__PURE__ */ jsx(UiMarkupBlock, {
1838
+ id: part.id,
1839
+ source: markup,
1840
+ streaming: part.status === "streaming" || part.status === "input"
1841
+ });
1842
+ }
1843
+ return { Block: UiMarkupToolBlock };
1844
+ }
1845
+ /** Reads `{ root, state }`. Models sometimes send `root` or `state` as a JSON string; those are parsed too. */
1846
+ function readTreeInput(input) {
1847
+ const i = input ?? {};
1848
+ const unwrap = (v) => typeof v === "string" ? salvageJson(v).value : v;
1849
+ const root = unwrap(i.root);
1850
+ const state = unwrap(i.state);
1851
+ return {
1852
+ ...root !== void 0 && { root },
1853
+ ...typeof state === "object" && state !== null && { state }
1854
+ };
1855
+ }
1856
+ /**
1857
+ * A tool view that draws a call's (streaming) input as a tree of your registered components, for whatever
1858
+ * tool your harness uses. Register it under your tool's name: `{ render_ui: uiTreeToolView() }`. `read` maps
1859
+ * your tool's input to the tree and its starting state; the default reads `{ root, state }`, the shape of
1860
+ * the suggested `renderUiTool`.
1861
+ */
1862
+ function uiTreeToolView(read = readTreeInput) {
1863
+ function UiTreeToolBlock({ part }) {
1864
+ const { root, state } = read(part.input ?? part.partialInput);
1865
+ if (!root) return null;
1866
+ const ok = typeof state === "object" && state !== null;
1867
+ return /* @__PURE__ */ jsx(UiTree, {
1868
+ id: part.id,
1869
+ root,
1870
+ ...ok && { state },
1871
+ streaming: part.status === "streaming" || part.status === "input"
1872
+ });
1873
+ }
1874
+ return { Block: UiTreeToolBlock };
1875
+ }
1876
+ /**
1877
+ * The escape hatch: raw HTML or SVG in a sandboxed WidgetFrame, as a node of the tree. Register it only if
1878
+ * the model may write HTML; it gets what the chat's `widgets` host allows.
1879
+ */
1880
+ const htmlComponent = {
1881
+ name: "Html",
1882
+ description: "Raw HTML or <svg> in a sandboxed frame, only for what the other components cannot show. Follow the visual design guide.",
1883
+ props: {
1884
+ type: "object",
1885
+ properties: {
1886
+ html: { type: "string" },
1887
+ title: { type: "string" }
1888
+ },
1889
+ required: ["html"]
1890
+ },
1891
+ render: function Html({ html, title }) {
1892
+ const { streaming } = useContext(TreeContext);
1893
+ const { allow, onPrompt, onOpenLink } = useWidgetHost();
1894
+ return /* @__PURE__ */ jsx(WidgetFrame, {
1895
+ html,
1896
+ streaming,
1897
+ ...title && { title },
1898
+ ...allow && { allow },
1899
+ ...onPrompt && { onPrompt },
1900
+ ...onOpenLink && { onOpenLink }
1901
+ });
1902
+ }
1903
+ };
1015
1904
  // src/components/word-fade.ts
1016
1905
  const OPAQUE_TAGS = /* @__PURE__ */ new Set([
1017
1906
  "code",
@@ -1078,45 +1967,96 @@ function textOf(node) {
1078
1967
  if (node.type === "text") return node.value ?? "";
1079
1968
  return (node.children ?? []).map(textOf).join("");
1080
1969
  }
1970
+ const BlockContext = createContext(void 0);
1971
+ function Code({ node, className, children }) {
1972
+ const hast = node;
1973
+ const language = /language-([\w+#-]+)/.exec(className ?? "")?.[1];
1974
+ const ui = useUiMarkupLanguage();
1975
+ const block = useContext(BlockContext);
1976
+ const multiline = hast?.position && hast.position.start.line !== hast.position.end.line;
1977
+ if (!language && !multiline) return /* @__PURE__ */ jsx("code", {
1978
+ "data-chat-inline-code": true,
1979
+ children
1980
+ });
1981
+ const code = textOf(hast).replace(/\n$/, "");
1982
+ if (block && ui && language === ui) return /* @__PURE__ */ jsx(UiMarkupBlock, {
1983
+ id: `${block.id}:${hast?.position?.start.line ?? 0}`,
1984
+ source: code,
1985
+ streaming: block.streaming
1986
+ });
1987
+ return /* @__PURE__ */ jsx(CodeBlock, {
1988
+ variant: "boxed",
1989
+ code,
1990
+ ...language && { language }
1991
+ });
1992
+ }
1081
1993
  const components = {
1082
1994
  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
- }
1995
+ code: ({ node, className, children }) => /* @__PURE__ */ jsx(Code, {
1996
+ node,
1997
+ className,
1998
+ children
1999
+ })
1097
2000
  };
2001
+ const LinksContext = createContext(void 0);
2002
+ function MarkdownLinksProvider({ renderLink, children }) {
2003
+ return /* @__PURE__ */ jsx(LinksContext.Provider, {
2004
+ value: renderLink,
2005
+ children
2006
+ });
2007
+ }
1098
2008
  /** Message markdown: GFM, KaTeX math ($…$ and $$…$$), Shiki code blocks, smooth fade-in while streaming. */
1099
- function Markdown({ children, streaming = false, className }) {
2009
+ function Markdown({ children, streaming = false, className, partId }) {
1100
2010
  const text = useSmoothText(children, streaming);
1101
2011
  const animating = streaming || text.length < children.length;
1102
2012
  const [streamed, setStreamed] = useState(streaming);
1103
2013
  if (streaming && !streamed) setStreamed(true);
1104
2014
  return /* @__PURE__ */ jsx("div", {
1105
2015
  className: clsx("chat-markdown", streamed && "chat-fade", className),
1106
- children: /* @__PURE__ */ jsx(MarkdownBody, {
1107
- text,
1108
- animating,
1109
- fade: streamed
2016
+ children: /* @__PURE__ */ jsx(BlockScope, {
2017
+ id: partId,
2018
+ streaming: animating,
2019
+ children: /* @__PURE__ */ jsx(MarkdownBody, {
2020
+ text,
2021
+ animating,
2022
+ fade: streamed
2023
+ })
1110
2024
  })
1111
2025
  });
1112
2026
  }
2027
+ function BlockScope({ id, streaming, children }) {
2028
+ const value = useMemo(() => id ? {
2029
+ id,
2030
+ streaming
2031
+ } : void 0, [id, streaming]);
2032
+ return /* @__PURE__ */ jsx(BlockContext.Provider, {
2033
+ value,
2034
+ children
2035
+ });
2036
+ }
1113
2037
  const MarkdownBody = memo(function MarkdownBody({ text, animating, fade }) {
2038
+ const renderLink = useContext(LinksContext);
2039
+ const withLinks = useMemo(() => renderLink ? {
2040
+ ...components,
2041
+ a: ({ href, children }) => {
2042
+ const custom = href ? renderLink({
2043
+ href,
2044
+ children
2045
+ }) : void 0;
2046
+ return custom !== void 0 ? /* @__PURE__ */ jsx(Fragment, { children: custom }) : /* @__PURE__ */ jsx("a", {
2047
+ href,
2048
+ target: "_blank",
2049
+ rel: "noopener noreferrer",
2050
+ children
2051
+ });
2052
+ }
2053
+ } : components, [renderLink]);
1114
2054
  return /* @__PURE__ */ jsx(Streamdown, {
1115
2055
  prefix: PREFIX,
1116
2056
  mode: animating || fade ? "streaming" : "static",
1117
2057
  ...fade && { rehypePlugins: fadePlugins },
1118
2058
  plugins: { math },
1119
- components,
2059
+ components: withLinks,
1120
2060
  controls: false,
1121
2061
  lineNumbers: false,
1122
2062
  linkSafety: { enabled: false },
@@ -1156,32 +2096,31 @@ const useChatTool = useAgentTool;
1156
2096
  /** Streaming or running: the model or a tool is busy with it. */
1157
2097
  const isWorking = (part) => part.status !== "done" && part.status !== "error" && part.status !== "awaiting";
1158
2098
  /** Row label for any activity part. */
1159
- function activityLabel(part, view) {
2099
+ function activityLabel(part, labels, view) {
1160
2100
  if (part.type === "thinking") {
1161
2101
  if (part.title) return part.title;
1162
- if (part.status !== "done") return "Thinking…";
1163
- return part.durationMs ? `Thought for ${formatDuration(part.durationMs)}` : "Thought";
2102
+ if (part.status !== "done") return labels.thinking;
2103
+ return part.durationMs ? labels.thoughtFor(formatDuration(part.durationMs, labels)) : labels.thought;
1164
2104
  }
1165
- return view?.label?.(part) ?? part.title ?? part.name;
2105
+ return view?.label?.(part, labels) ?? part.title ?? part.name;
1166
2106
  }
1167
2107
  /**
1168
2108
  * Heading of an activity group. While live it follows the latest step. Once done it lists what the tools
1169
2109
  * did, in order ("Ran a command, searched the web"); a group of only thinking shows the first title.
1170
2110
  */
1171
- function activityHeading(parts, views) {
2111
+ function activityHeading(parts, views, labels) {
1172
2112
  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"] });
2113
+ if (waiting) return /* @__PURE__ */ jsxs(Fragment, { children: [activityLabel(waiting, labels, views[waiting.name]), labels.waitingForYou] });
1174
2114
  if (parts.some(isWorking)) {
1175
2115
  const last = parts.at(-1);
1176
- return activityLabel(last, last.type === "tool_call" ? views[last.name] : void 0);
2116
+ return activityLabel(last, labels, last.type === "tool_call" ? views[last.name] : void 0);
1177
2117
  }
1178
2118
  const counts = /* @__PURE__ */ new Map();
1179
2119
  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);
2120
+ if (counts.size === 0) return parts[0] ? activityLabel(parts[0], labels) : "";
2121
+ const phrases = [...counts].map(([name, count]) => views[name]?.summary?.(count, labels) ?? labels.usedTool(name));
2122
+ return labels.joinSummaries(phrases);
1183
2123
  }
1184
- const plural = (count, one, many) => count === 1 ? one : many(count);
1185
2124
  function recordOf(value) {
1186
2125
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1187
2126
  }
@@ -1190,10 +2129,11 @@ function stringOf(value) {
1190
2129
  }
1191
2130
  /** A shell command. Input `{ command, description? }`; result a string or `{ stdout, stderr, exitCode }`. */
1192
2131
  const commandTool = {
1193
- label: () => "Ran a command",
2132
+ label: (_, labels) => labels.ranCommandLabel,
1194
2133
  hint: (part) => stringOf(recordOf(part.input).description),
1195
- summary: (n) => plural(n, "ran a command", (n) => `ran ${n} commands`),
2134
+ summary: (n, labels) => labels.ranCommands(n),
1196
2135
  Detail: function CommandDetail({ part }) {
2136
+ const labels = useLabels();
1197
2137
  const command = stringOf(recordOf(part.input).command) ?? part.inputJson;
1198
2138
  const result = recordOf(part.result);
1199
2139
  const output = stringOf(part.result) ?? [stringOf(result.stdout), stringOf(result.stderr)].filter(Boolean).join("\n");
@@ -1205,31 +2145,37 @@ const commandTool = {
1205
2145
  code: command
1206
2146
  }),
1207
2147
  (output || part.status === "done") && /* @__PURE__ */ jsx(CodeBlock, {
1208
- label: "Output",
1209
- code: output || "(no output)"
2148
+ label: labels.output,
2149
+ code: output || labels.noOutput
1210
2150
  }),
1211
2151
  part.error && /* @__PURE__ */ jsx("p", {
1212
- className: "text-sm text-chat-danger",
2152
+ className: "text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
1213
2153
  children: part.error
1214
2154
  })
1215
2155
  ]
1216
2156
  });
1217
2157
  }
1218
2158
  };
1219
- /** A web search. Input `{ query }`; result `{ results: SearchResult[] }` or `SearchResult[]`. */
2159
+ /**
2160
+ * A web search. Input `{ query }`; results (`{ results: SearchResult[] }` or `SearchResult[]`) from the
2161
+ * part's display `data`, else its `result`.
2162
+ */
1220
2163
  const webSearchTool = {
1221
- label: () => "Searched the web",
2164
+ label: (_, labels) => labels.searchedWebLabel,
1222
2165
  subtitle: (part) => stringOf(recordOf(part.input).query) ?? part.subtitle,
1223
- summary: (n) => plural(n, "searched the web", (n) => `searched the web ${n} times`),
2166
+ summary: (n, labels) => labels.searchedWeb(n),
1224
2167
  Detail: function WebSearchDetail({ part }) {
1225
- const results = Array.isArray(part.result) ? part.result : recordOf(part.result).results;
2168
+ const labels = useLabels();
2169
+ const icons = useIcons();
2170
+ const shown = part.data ?? part.result;
2171
+ const results = Array.isArray(shown) ? shown : recordOf(shown).results;
1226
2172
  if (part.error) return /* @__PURE__ */ jsx("p", {
1227
- className: "text-sm text-chat-danger",
2173
+ className: "text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
1228
2174
  children: part.error
1229
2175
  });
1230
2176
  if (!results?.length) return /* @__PURE__ */ jsx("p", {
1231
- className: "text-sm text-chat-muted",
1232
- children: "No results"
2177
+ className: "text-(length:--chat-ui-font-size) leading-5 text-chat-muted",
2178
+ children: labels.noResults
1233
2179
  });
1234
2180
  return /* @__PURE__ */ jsx("ul", {
1235
2181
  className: "-mx-1",
@@ -1237,22 +2183,22 @@ const webSearchTool = {
1237
2183
  href: result.url,
1238
2184
  target: "_blank",
1239
2185
  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",
2186
+ 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
2187
  children: [
1242
2188
  result.favicon ? /* @__PURE__ */ jsx("img", {
1243
2189
  src: result.favicon,
1244
2190
  alt: "",
1245
2191
  className: "size-4 shrink-0 rounded-sm"
1246
- }) : /* @__PURE__ */ jsx(Globe, {
1247
- "aria-hidden": true,
1248
- className: "size-4 shrink-0"
2192
+ }) : /* @__PURE__ */ jsx(IconSlot, {
2193
+ className: "[&_svg]:size-4",
2194
+ children: icons.web
1249
2195
  }),
1250
2196
  /* @__PURE__ */ jsx("span", {
1251
2197
  className: "min-w-0 flex-1 truncate",
1252
2198
  children: result.title
1253
2199
  }),
1254
2200
  /* @__PURE__ */ jsx("span", {
1255
- className: "shrink-0 text-sm",
2201
+ className: "shrink-0 text-(length:--chat-ui-font-size) leading-5",
1256
2202
  children: hostOf(result.url)
1257
2203
  })
1258
2204
  ]
@@ -1260,18 +2206,44 @@ const webSearchTool = {
1260
2206
  });
1261
2207
  }
1262
2208
  };
2209
+ /** Reads a web page. Input `{ url }`; the row shows the page's host, the detail links to it. */
2210
+ const webFetchTool = {
2211
+ label: (_, labels) => labels.readPageLabel,
2212
+ subtitle: (part) => {
2213
+ const url = stringOf(recordOf(part.input).url);
2214
+ return url ? hostOf(url) : part.subtitle;
2215
+ },
2216
+ summary: (n, labels) => labels.readPages(n),
2217
+ Detail: function WebFetchDetail({ part }) {
2218
+ const url = stringOf(recordOf(part.input).url);
2219
+ const title = stringOf(recordOf(part.data).title);
2220
+ if (part.error) return /* @__PURE__ */ jsx("p", {
2221
+ className: "text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
2222
+ children: part.error
2223
+ });
2224
+ if (!url) return null;
2225
+ return /* @__PURE__ */ jsx("a", {
2226
+ href: url,
2227
+ target: "_blank",
2228
+ rel: "noopener noreferrer",
2229
+ className: "block truncate text-(length:--chat-ui-font-size) leading-5 text-chat-fg hover:underline",
2230
+ children: title ?? url
2231
+ });
2232
+ }
2233
+ };
1263
2234
  /** Memory lookups, e.g. Anthropic's memory tool. Input `{ path }`. */
1264
2235
  const memoryTool = {
1265
- label: () => "Recalled memory",
2236
+ label: (_, labels) => labels.recalledMemoryLabel,
1266
2237
  subtitle: (part) => {
1267
2238
  return stringOf(recordOf(part.input).path)?.split("/").at(-1) ?? part.subtitle;
1268
2239
  },
1269
- summary: () => "recalled memory"
2240
+ summary: (n, labels) => labels.recalledMemory(n)
1270
2241
  };
1271
2242
  const defaultToolViews = {
1272
2243
  bash: commandTool,
1273
2244
  shell: commandTool,
1274
2245
  web_search: webSearchTool,
2246
+ web_fetch: webFetchTool,
1275
2247
  memory: memoryTool,
1276
2248
  memory_view: memoryTool
1277
2249
  };
@@ -1283,9 +2255,8 @@ function hostOf(url) {
1283
2255
  }
1284
2256
  }
1285
2257
  /** "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`;
2258
+ function formatDuration(ms, labels = defaultLabels) {
2259
+ return labels.duration(Math.max(1, Math.round(ms / 1e3)));
1289
2260
  }
1290
2261
  // src/components/token-counter.tsx
1291
2262
  /** Compact token count: 526, 1.4k, 33k, 76.8k, 1M. */
@@ -1330,25 +2301,23 @@ function useCountUp(target) {
1330
2301
  }
1331
2302
  /** A token count that counts up as `tokens` grows and holds still when it stops growing. */
1332
2303
  function TokenCounter({ tokens, variant = "standalone", className }) {
2304
+ const icons = useIcons();
1333
2305
  const shown = useCountUp(tokens);
2306
+ const labels = useLabels();
1334
2307
  if (variant === "inline") return /* @__PURE__ */ jsxs("span", {
1335
2308
  className: clsx("text-chat-subtle tabular-nums", className),
1336
2309
  children: [
1337
2310
  "(",
1338
- formatTokens(shown),
1339
- " tokens)"
2311
+ labels.tokenCount(formatTokens(shown)),
2312
+ ")"
1340
2313
  ]
1341
2314
  });
1342
2315
  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
- ]
2316
+ 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),
2317
+ children: [/* @__PURE__ */ jsx(IconSlot, {
2318
+ className: "[&_svg]:size-3",
2319
+ children: icons.tokens
2320
+ }), labels.tokenCount(formatTokens(shown))]
1352
2321
  });
1353
2322
  }
1354
2323
  // src/components/activity-group.tsx
@@ -1359,6 +2328,7 @@ function TokenCounter({ tokens, variant = "standalone", className }) {
1359
2328
  */
1360
2329
  function ActivityGroup({ parts, defaultExpanded = false, className }) {
1361
2330
  const views = useToolViews();
2331
+ const labels = useLabels();
1362
2332
  const [expanded, setExpanded] = useState(defaultExpanded);
1363
2333
  const live = parts.some(isWorking);
1364
2334
  const tokens = activityTokens(parts);
@@ -1380,9 +2350,9 @@ function ActivityGroup({ parts, defaultExpanded = false, className }) {
1380
2350
  onExpandedChange: setExpanded,
1381
2351
  summary: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Live, {
1382
2352
  live,
1383
- children: activityHeading(parts, views)
2353
+ children: activityHeading(parts, views, labels)
1384
2354
  }), after] }),
1385
- summaryClassName: "text-[0.9375rem]",
2355
+ summaryClassName: "text-(length:--chat-activity-font-size)",
1386
2356
  panelClassName: "pt-3",
1387
2357
  children: /* @__PURE__ */ jsx("div", {
1388
2358
  className: "divide-y divide-chat-divider rounded-chat-md border border-chat-divider",
@@ -1419,7 +2389,7 @@ function SingleStep({ part, live, after, expanded, onExpandedChange }) {
1419
2389
  live,
1420
2390
  children: summary
1421
2391
  }), after] }),
1422
- summaryClassName: "text-[0.9375rem]",
2392
+ summaryClassName: "text-(length:--chat-activity-font-size)",
1423
2393
  panelClassName: "pt-3",
1424
2394
  children: /* @__PURE__ */ jsx("div", {
1425
2395
  className: "rounded-chat-md border border-chat-divider px-3 py-2",
@@ -1432,7 +2402,7 @@ function Step({ part }) {
1432
2402
  const live = isWorking(part);
1433
2403
  return /* @__PURE__ */ jsx(Disclosure, {
1434
2404
  className: "px-3 py-2",
1435
- summaryClassName: "text-[0.9375rem]",
2405
+ summaryClassName: "text-(length:--chat-activity-font-size)",
1436
2406
  panelClassName: "pt-2 pb-1",
1437
2407
  summary: /* @__PURE__ */ jsx(Live, {
1438
2408
  live,
@@ -1443,12 +2413,13 @@ function Step({ part }) {
1443
2413
  }
1444
2414
  /** The label line and expanded body of one step. Tool calls go through their registered view. */
1445
2415
  function useStepContent(part) {
2416
+ const labels = useLabels();
1446
2417
  const view = useToolView(part.type === "tool_call" ? part.name : "");
1447
2418
  if (part.type === "thinking") return {
1448
- summary: activityLabel(part),
2419
+ summary: activityLabel(part, labels),
1449
2420
  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."
2421
+ className: "text-(length:--chat-ui-font-size) leading-5 text-chat-subtle",
2422
+ children: labels.reasoningHidden
1452
2423
  }) : /* @__PURE__ */ jsx("div", {
1453
2424
  className: "max-h-96 overflow-y-auto [scrollbar-width:thin]",
1454
2425
  children: /* @__PURE__ */ jsx(Markdown, {
@@ -1463,7 +2434,7 @@ function useStepContent(part) {
1463
2434
  return {
1464
2435
  summary: /* @__PURE__ */ jsxs("span", {
1465
2436
  title: view.hint?.(part),
1466
- children: [activityLabel(part, view), subtitle && /* @__PURE__ */ jsx("span", {
2437
+ children: [activityLabel(part, labels, view), subtitle && /* @__PURE__ */ jsx("span", {
1467
2438
  className: "ml-1.5 text-chat-fg",
1468
2439
  children: subtitle
1469
2440
  })]
@@ -1473,16 +2444,16 @@ function useStepContent(part) {
1473
2444
  }
1474
2445
  function DefaultDetail({ part }) {
1475
2446
  if (part.error) return /* @__PURE__ */ jsx("p", {
1476
- className: "text-sm text-chat-danger",
2447
+ className: "text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
1477
2448
  children: part.error
1478
2449
  });
1479
2450
  const body = part.result ?? part.input ?? part.inputJson;
1480
2451
  if (typeof body === "string") return /* @__PURE__ */ jsx("p", {
1481
- className: "text-[0.9375rem] leading-relaxed whitespace-pre-wrap text-chat-muted",
2452
+ className: "text-(length:--chat-activity-font-size) leading-relaxed whitespace-pre-wrap text-chat-muted",
1482
2453
  children: body
1483
2454
  });
1484
2455
  return /* @__PURE__ */ jsx("pre", {
1485
- className: "max-h-56 overflow-auto font-chat-mono text-[0.8125rem] leading-relaxed text-chat-muted",
2456
+ className: "max-h-56 overflow-auto font-chat-mono text-(length:--chat-code-font-size) leading-relaxed text-chat-muted",
1486
2457
  children: JSON.stringify(body, null, 2)
1487
2458
  });
1488
2459
  }
@@ -1493,6 +2464,9 @@ const draftId = () => `att_${Date.now().toString(36)}_${(counter++).toString(36)
1493
2464
  function useAttachments({ upload } = {}) {
1494
2465
  const [attachments, setAttachments] = useState([]);
1495
2466
  const uploads = useRef(/* @__PURE__ */ new Map());
2467
+ const labels = useLabels();
2468
+ const labelsRef = useRef(labels);
2469
+ labelsRef.current = labels;
1496
2470
  const uploadRef = useRef(upload);
1497
2471
  uploadRef.current = upload;
1498
2472
  const patch = useCallback((id, fields) => {
@@ -1526,7 +2500,7 @@ function useAttachments({ upload } = {}) {
1526
2500
  })).catch((error) => {
1527
2501
  if (!controller.signal.aborted) patch(id, {
1528
2502
  status: "error",
1529
- error: error instanceof Error ? error.message : "Upload failed"
2503
+ error: error instanceof Error ? error.message : labelsRef.current.uploadFailed
1530
2504
  });
1531
2505
  }).finally(() => uploads.current.delete(id));
1532
2506
  }
@@ -1535,7 +2509,7 @@ function useAttachments({ upload } = {}) {
1535
2509
  setAttachments((list) => [...list, {
1536
2510
  id: draftId(),
1537
2511
  kind: "text",
1538
- name: "Pasted text",
2512
+ name: labelsRef.current.pastedText,
1539
2513
  mime: "text/plain",
1540
2514
  size: new Blob([text]).size,
1541
2515
  text,
@@ -1581,13 +2555,15 @@ function useAttachments({ upload } = {}) {
1581
2555
  const extension = (name) => /\.([a-z0-9]{1,5})$/i.exec(name)?.[1]?.toUpperCase();
1582
2556
  /** A square card: pasted text preview with a PASTED badge, an image thumbnail, or a file with its type. */
1583
2557
  function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", className }) {
2558
+ const icons = useIcons();
1584
2559
  const image = a.mime.startsWith("image/") && a.url;
1585
2560
  const pasted = a.text !== void 0;
2561
+ const labels = useLabels();
1586
2562
  const uploading = a.status === "uploading";
1587
2563
  const failed = a.status === "error";
1588
2564
  return /* @__PURE__ */ jsxs("div", {
1589
2565
  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,
2566
+ title: failed ? a.error ?? labels.uploadFailed : a.name,
1591
2567
  children: [
1592
2568
  /* @__PURE__ */ jsx("div", {
1593
2569
  className: clsx("absolute inset-0 overflow-hidden rounded-[inherit]", uploading && "chat-shimmer-surface"),
@@ -1600,11 +2576,11 @@ function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", classNam
1600
2576
  children: a.text
1601
2577
  }) : /* @__PURE__ */ jsxs("div", {
1602
2578
  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"
2579
+ children: [/* @__PURE__ */ jsx(IconSlot, {
2580
+ className: "text-chat-muted [&_svg]:size-5",
2581
+ children: icons.file
1606
2582
  }), /* @__PURE__ */ jsx("span", {
1607
- className: "line-clamp-3 text-xs break-words text-chat-fg",
2583
+ className: "line-clamp-3 text-(length:--chat-ui-font-size-sm) leading-4 break-words text-chat-fg",
1608
2584
  children: a.name
1609
2585
  })]
1610
2586
  })
@@ -1613,29 +2589,29 @@ function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", classNam
1613
2589
  className: "absolute inset-x-2 bottom-2 flex items-end justify-between gap-1",
1614
2590
  children: [/* @__PURE__ */ jsx("span", {
1615
2591
  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"
2592
+ children: failed ? labels.failed : pasted ? labels.pasted : extension(a.name) ?? labels.file
1617
2593
  }), pasted && onOpen && /* @__PURE__ */ jsx(IconButton, {
1618
- label: "Edit pasted text",
2594
+ label: labels.editPastedText,
1619
2595
  size: "sm",
1620
2596
  onPress: onOpen,
1621
2597
  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, {})
2598
+ children: icons.edit
1623
2599
  })]
1624
2600
  }),
1625
2601
  pasted && onOpen && !onRemove && /* @__PURE__ */ jsx("button", {
1626
2602
  type: "button",
1627
- "aria-label": "View pasted text",
2603
+ "aria-label": labels.viewPastedText,
1628
2604
  onClick: onOpen,
1629
2605
  className: "absolute inset-0 cursor-pointer rounded-[inherit] outline-none focus-visible:ring-2 focus-visible:ring-chat-focus"
1630
2606
  }),
1631
2607
  onRemove && /* @__PURE__ */ jsx(IconButton, {
1632
- label: `Remove ${a.name}`,
2608
+ label: labels.removeAttachment(a.name),
1633
2609
  variant: "subtle",
1634
2610
  size: "sm",
1635
2611
  tooltip: false,
1636
2612
  onPress: onRemove,
1637
2613
  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, {})
2614
+ children: icons.remove
1639
2615
  })
1640
2616
  ]
1641
2617
  });
@@ -1643,9 +2619,10 @@ function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", classNam
1643
2619
  /** The composer's row of cards above the text. Renders nothing when empty. */
1644
2620
  function AttachmentRow({ attachments, onRemove, onEditText }) {
1645
2621
  const [editing, setEditing] = useState();
2622
+ const labels = useLabels();
1646
2623
  if (attachments.length === 0) return null;
1647
2624
  return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("ul", {
1648
- "aria-label": "Attachments",
2625
+ "aria-label": labels.attachments,
1649
2626
  className: "flex gap-2.5 overflow-x-auto px-0.5 pt-2.5 pb-1 [scrollbar-width:thin]",
1650
2627
  children: attachments.map((a) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(AttachmentCard, {
1651
2628
  attachment: {
@@ -1662,7 +2639,9 @@ function AttachmentRow({ attachments, onRemove, onEditText }) {
1662
2639
  })] });
1663
2640
  }
1664
2641
  /** Shows pasted text in a modal, editable when `onSave` is set. */
1665
- function PastedTextDialog({ text, onClose, onSave, title = "Pasted text" }) {
2642
+ function PastedTextDialog({ text, onClose, onSave, title }) {
2643
+ const labels = useLabels();
2644
+ title ??= labels.pastedText;
1666
2645
  const [draft, setDraft] = useState(text ?? "");
1667
2646
  useEffect(() => setDraft(text ?? ""), [text]);
1668
2647
  return /* @__PURE__ */ jsxs(Modal, {
@@ -1674,34 +2653,34 @@ function PastedTextDialog({ text, onClose, onSave, title = "Pasted text" }) {
1674
2653
  onChange: (e) => setDraft(e.target.value),
1675
2654
  readOnly: !onSave,
1676
2655
  "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]"
2656
+ 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
2657
  }), /* @__PURE__ */ jsxs("div", {
1679
2658
  className: "flex justify-end gap-2 px-5 py-4",
1680
2659
  children: [/* @__PURE__ */ jsx(Button, {
1681
2660
  variant: "subtle",
1682
2661
  onPress: onClose,
1683
- children: onSave ? "Cancel" : "Close"
2662
+ children: onSave ? labels.cancel : labels.close
1684
2663
  }), onSave && /* @__PURE__ */ jsx(Button, {
1685
2664
  variant: "primary",
1686
2665
  onPress: () => {
1687
2666
  onSave(draft);
1688
2667
  onClose();
1689
2668
  },
1690
- children: "Save"
2669
+ children: labels.save
1691
2670
  })]
1692
2671
  })]
1693
2672
  });
1694
2673
  }
1695
2674
  // src/components/messages.tsx
1696
2675
  /** "just now", "5 minutes ago", "21 hours ago", "yesterday", then a date. */
1697
- function formatRelativeTime(iso, now = Date.now()) {
2676
+ function formatRelativeTime(iso, now = Date.now(), labels = defaultLabels) {
1698
2677
  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";
2678
+ const rtf = new Intl.RelativeTimeFormat(labels.locale, { numeric: "auto" });
2679
+ if (Math.abs(seconds) < 60) return labels.justNow;
1701
2680
  if (Math.abs(seconds) < 3600) return rtf.format(Math.round(seconds / 60), "minute");
1702
2681
  if (Math.abs(seconds) < 86400) return rtf.format(Math.round(seconds / 3600), "hour");
1703
2682
  if (Math.abs(seconds) < 604800) return rtf.format(Math.round(seconds / 86400), "day");
1704
- return new Date(iso).toLocaleDateString(void 0, {
2683
+ return new Date(iso).toLocaleDateString(labels.locale, {
1705
2684
  month: "short",
1706
2685
  day: "numeric"
1707
2686
  });
@@ -1711,21 +2690,25 @@ function messageText(message) {
1711
2690
  return message.parts.map((p) => p.type === "text" ? p.text : p.type === "custom" ? p.text ?? "" : "").filter(Boolean).join("\n\n");
1712
2691
  }
1713
2692
  function CopyButton({ text }) {
2693
+ const icons = useIcons();
2694
+ const labels = useLabels();
1714
2695
  const [copied, setCopied] = useState(false);
1715
2696
  return /* @__PURE__ */ jsx(IconButton, {
1716
- label: copied ? "Copied" : "Copy",
2697
+ label: copied ? labels.copied : labels.copy,
1717
2698
  size: "sm",
1718
2699
  onPress: () => {
1719
2700
  navigator.clipboard?.writeText(text);
1720
2701
  setCopied(true);
1721
2702
  setTimeout(() => setCopied(false), 1500);
1722
2703
  },
1723
- children: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {})
2704
+ children: copied ? icons.copied : icons.copy
1724
2705
  });
1725
2706
  }
1726
- const timeClass = "px-1 font-chat-sans text-xs text-chat-subtle";
2707
+ const timeClass = "px-1 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle";
1727
2708
  /** The user's turn: a bubble on the right, with time, retry, edit and copy under it on hover. */
1728
2709
  function UserMessage({ message, onRetry, onEdit, actions, className }) {
2710
+ const icons = useIcons();
2711
+ const labels = useLabels();
1729
2712
  const text = messageText(message);
1730
2713
  const attachments = message.parts.filter((p) => p.type === "attachment");
1731
2714
  const [viewing, setViewing] = useState();
@@ -1733,7 +2716,7 @@ function UserMessage({ message, onRetry, onEdit, actions, className }) {
1733
2716
  className: clsx("group/message flex flex-col items-end", className),
1734
2717
  children: [
1735
2718
  attachments.length > 0 && /* @__PURE__ */ jsx("ul", {
1736
- "aria-label": "Attachments",
2719
+ "aria-label": labels.attachments,
1737
2720
  className: "mb-2 flex max-w-[85%] flex-wrap justify-end gap-2",
1738
2721
  children: attachments.map((a) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(AttachmentCard, {
1739
2722
  attachment: a,
@@ -1745,28 +2728,25 @@ function UserMessage({ message, onRetry, onEdit, actions, className }) {
1745
2728
  text: viewing,
1746
2729
  onClose: () => setViewing(void 0)
1747
2730
  }),
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
- }),
2731
+ text && /* @__PURE__ */ jsx(UserBubble, { text }),
1752
2732
  /* @__PURE__ */ jsxs("div", {
1753
2733
  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
2734
  children: [
1755
2735
  /* @__PURE__ */ jsx("span", {
1756
2736
  className: timeClass,
1757
- children: formatRelativeTime(message.createdAt)
2737
+ children: formatRelativeTime(message.createdAt, void 0, labels)
1758
2738
  }),
1759
2739
  onRetry && /* @__PURE__ */ jsx(IconButton, {
1760
- label: "Retry",
2740
+ label: labels.retry,
1761
2741
  size: "sm",
1762
2742
  onPress: onRetry,
1763
- children: /* @__PURE__ */ jsx(RefreshCw, {})
2743
+ children: icons.retry
1764
2744
  }),
1765
2745
  onEdit && /* @__PURE__ */ jsx(IconButton, {
1766
- label: "Edit",
2746
+ label: labels.edit,
1767
2747
  size: "sm",
1768
2748
  onPress: onEdit,
1769
- children: /* @__PURE__ */ jsx(Pencil, {})
2749
+ children: icons.edit
1770
2750
  }),
1771
2751
  /* @__PURE__ */ jsx(CopyButton, { text }),
1772
2752
  actions
@@ -1775,11 +2755,46 @@ function UserMessage({ message, onRetry, onEdit, actions, className }) {
1775
2755
  ]
1776
2756
  });
1777
2757
  }
2758
+ /** Taller than this (px), a user message folds to this height with "Show more", like a long paste. */
2759
+ const FOLD_HEIGHT = 240;
2760
+ function UserBubble({ text }) {
2761
+ const body = useRef(null);
2762
+ const [long, setLong] = useState(false);
2763
+ const [open, setOpen] = useState(false);
2764
+ const labels = useLabels();
2765
+ useLayoutEffect(() => {
2766
+ const el = body.current;
2767
+ if (!el) return;
2768
+ const measure = () => setLong(el.scrollHeight > 264);
2769
+ measure();
2770
+ const observer = new ResizeObserver(measure);
2771
+ observer.observe(el);
2772
+ return () => observer.disconnect();
2773
+ }, [text]);
2774
+ const folded = long && !open;
2775
+ return /* @__PURE__ */ jsxs("div", {
2776
+ 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",
2777
+ children: [/* @__PURE__ */ jsx("div", {
2778
+ ref: body,
2779
+ style: folded ? { maxHeight: FOLD_HEIGHT } : void 0,
2780
+ className: clsx("whitespace-pre-wrap", folded && "overflow-hidden mask-b-from-60% mask-b-to-100%"),
2781
+ children: text
2782
+ }), long && /* @__PURE__ */ jsx("button", {
2783
+ type: "button",
2784
+ "aria-expanded": open,
2785
+ onClick: () => setOpen(!open),
2786
+ 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",
2787
+ children: open ? labels.showLess : labels.showMore
2788
+ })]
2789
+ });
2790
+ }
1778
2791
  /**
1779
2792
  * The assistant's turn: activity groups (each with its own token count), markdown, and other parts in
1780
2793
  * order. When done, copy / read aloud / feedback / retry and the time sit under it.
1781
2794
  */
1782
2795
  function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPart, actions, className }) {
2796
+ const icons = useIcons();
2797
+ const labels = useLabels();
1783
2798
  const streaming = message.status === "streaming";
1784
2799
  const views = useToolViews();
1785
2800
  const blocks = toBlocks(message.parts, (part) => views[part.name]?.Block !== void 0);
@@ -1787,56 +2802,66 @@ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPar
1787
2802
  className: clsx("group/message flex flex-col gap-4", className),
1788
2803
  children: [
1789
2804
  blocks.map((block) => {
1790
- if (block.kind === "activity") return /* @__PURE__ */ jsx(ActivityGroup, { parts: block.parts }, block.id);
2805
+ if (block.kind === "activity") {
2806
+ const cards = block.parts.filter((p) => p.type === "tool_call" && views[p.name]?.Card);
2807
+ if (!cards.length) return /* @__PURE__ */ jsx(ActivityGroup, { parts: block.parts }, block.id);
2808
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ActivityGroup, { parts: block.parts }), cards.map((p) => {
2809
+ if (p.type !== "tool_call") return null;
2810
+ const Card = views[p.name].Card;
2811
+ return /* @__PURE__ */ jsx(Card, { part: p }, p.id);
2812
+ })] }, block.id);
2813
+ }
1791
2814
  if (block.kind === "tool") {
1792
2815
  const ToolBlock = views[block.part.name].Block;
1793
2816
  return /* @__PURE__ */ jsx(ToolBlock, { part: block.part }, block.id);
1794
2817
  }
1795
2818
  const part = block.part;
1796
2819
  if (part.type === "text") return /* @__PURE__ */ jsx(Markdown, {
2820
+ partId: part.id,
1797
2821
  streaming: part.status === "streaming",
1798
2822
  children: part.text
1799
2823
  }, part.id);
1800
2824
  const custom = renderPart?.(part);
1801
2825
  if (custom) return /* @__PURE__ */ jsx("div", { children: custom }, part.id);
2826
+ if (part.type === "widget") return /* @__PURE__ */ jsx(WidgetPartView, { part }, part.id);
1802
2827
  if (part.type === "custom" && part.text) return /* @__PURE__ */ jsx(Markdown, { children: part.text }, part.id);
1803
2828
  return null;
1804
2829
  }),
1805
2830
  message.status === "error" && /* @__PURE__ */ jsx("p", {
1806
- className: "font-chat-sans text-sm text-chat-danger",
1807
- children: message.error ?? "Something went wrong."
2831
+ className: "font-chat-sans text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
2832
+ children: message.error ?? labels.somethingWentWrong
1808
2833
  }),
1809
2834
  streaming ? /* @__PURE__ */ jsx("div", { className: "h-7" }) : /* @__PURE__ */ jsxs("div", {
1810
2835
  className: "-ml-1.5 flex h-7 items-center gap-0.5",
1811
2836
  children: [
1812
2837
  /* @__PURE__ */ jsx(CopyButton, { text: messageText(message) }),
1813
2838
  onReadAloud && /* @__PURE__ */ jsx(IconButton, {
1814
- label: "Read aloud",
2839
+ label: labels.readAloud,
1815
2840
  size: "sm",
1816
2841
  onPress: onReadAloud,
1817
- children: /* @__PURE__ */ jsx(Volume2, {})
2842
+ children: icons.speak
1818
2843
  }),
1819
2844
  onFeedback && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(IconButton, {
1820
- label: "Good response",
2845
+ label: labels.goodResponse,
1821
2846
  size: "sm",
1822
2847
  onPress: () => onFeedback("good"),
1823
- children: /* @__PURE__ */ jsx(ThumbsUp, {})
2848
+ children: icons.thumbsUp
1824
2849
  }), /* @__PURE__ */ jsx(IconButton, {
1825
- label: "Bad response",
2850
+ label: labels.badResponse,
1826
2851
  size: "sm",
1827
2852
  onPress: () => onFeedback("bad"),
1828
- children: /* @__PURE__ */ jsx(ThumbsDown, {})
2853
+ children: icons.thumbsDown
1829
2854
  })] }),
1830
2855
  onRetry && /* @__PURE__ */ jsx(IconButton, {
1831
- label: "Retry",
2856
+ label: labels.retry,
1832
2857
  size: "sm",
1833
2858
  onPress: onRetry,
1834
- children: /* @__PURE__ */ jsx(RefreshCw, {})
2859
+ children: icons.retry
1835
2860
  }),
1836
2861
  actions,
1837
2862
  /* @__PURE__ */ jsx("span", {
1838
2863
  className: timeClass,
1839
- children: formatRelativeTime(message.createdAt)
2864
+ children: formatRelativeTime(message.createdAt, void 0, labels)
1840
2865
  })
1841
2866
  ]
1842
2867
  })
@@ -1849,7 +2874,9 @@ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPar
1849
2874
  * view's `Detail` when it has one, else the input. Replying in the composer instead declines it.
1850
2875
  */
1851
2876
  function ApprovalCard({ approval, onRespond, className }) {
2877
+ const icons = useIcons();
1852
2878
  const { part } = approval;
2879
+ const labels = useLabels();
1853
2880
  const view = useToolView(part.name);
1854
2881
  const Detail = view.Detail;
1855
2882
  const preview = {
@@ -1858,21 +2885,17 @@ function ApprovalCard({ approval, onRespond, className }) {
1858
2885
  };
1859
2886
  return /* @__PURE__ */ jsxs("div", {
1860
2887
  role: "alertdialog",
1861
- "aria-label": `Allow ${part.name}?`,
2888
+ "aria-label": labels.allowTool(part.name),
1862
2889
  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
2890
  children: [
1864
2891
  /* @__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"
2892
+ className: "flex items-center gap-2 text-(length:--chat-ui-font-size) leading-5",
2893
+ children: [/* @__PURE__ */ jsx(IconSlot, {
2894
+ className: "text-chat-accent [&_svg]:size-4",
2895
+ children: icons.approval
1869
2896
  }), /* @__PURE__ */ jsx("span", {
1870
2897
  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
- ] })
2898
+ children: view.ask?.(part) ?? labels.allowAction(activityLabel(part, labels, view))
1876
2899
  })]
1877
2900
  }),
1878
2901
  /* @__PURE__ */ jsx("div", {
@@ -1889,12 +2912,12 @@ function ApprovalCard({ approval, onRespond, className }) {
1889
2912
  variant: "ghost",
1890
2913
  className: "border border-chat-border-strong",
1891
2914
  onPress: () => onRespond(part.id, false),
1892
- children: "Deny"
2915
+ children: labels.deny
1893
2916
  }), /* @__PURE__ */ jsx(Button, {
1894
2917
  variant: "primary",
1895
2918
  autoFocus: true,
1896
2919
  onPress: () => onRespond(part.id, true),
1897
- children: "Allow"
2920
+ children: labels.allow
1898
2921
  })]
1899
2922
  })
1900
2923
  ]
@@ -1907,7 +2930,8 @@ const DEFAULT_COLOR = "var(--chat-subtle)";
1907
2930
  * What fills the context window: a usage line, a segmented bar, one row per category with its share, and
1908
2931
  * expandable lists (each MCP tool, each memory file). Deferred categories are listed but not counted.
1909
2932
  */
1910
- function ContextPanel({ usage, title = "Context", defaultExpanded = true, className }) {
2933
+ function ContextPanel({ usage, title, defaultExpanded = true, className }) {
2934
+ const labels = useLabels();
1911
2935
  const counted = usage.categories.filter((c) => !c.deferred);
1912
2936
  const deferred = usage.categories.filter((c) => c.deferred);
1913
2937
  const used = counted.reduce((sum, c) => sum + (c.reserved ? 0 : c.tokens), 0);
@@ -1915,16 +2939,16 @@ function ContextPanel({ usage, title = "Context", defaultExpanded = true, classN
1915
2939
  const free = Math.max(usage.limit - used - reserved, 0);
1916
2940
  const share = (tokens) => formatShare(tokens / usage.limit);
1917
2941
  return /* @__PURE__ */ jsxs(Disclosure, {
1918
- summary: title,
2942
+ summary: title ?? labels.context,
1919
2943
  defaultExpanded,
1920
2944
  className: clsx("font-chat-sans text-chat-fg", className),
1921
- summaryClassName: "text-[0.9375rem] text-chat-fg",
2945
+ summaryClassName: "text-(length:--chat-activity-font-size) text-chat-fg",
1922
2946
  panelClassName: "pt-3",
1923
2947
  children: [
1924
2948
  /* @__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",
2949
+ className: "mb-2 flex items-baseline justify-between text-(length:--chat-activity-font-size)",
2950
+ children: [/* @__PURE__ */ jsx("span", { children: labels.percentUsed(Math.round(used / usage.limit * 100)) }), /* @__PURE__ */ jsxs("span", {
2951
+ className: "text-(length:--chat-ui-font-size) leading-5 text-chat-muted tabular-nums",
1928
2952
  children: [
1929
2953
  formatTokens(used),
1930
2954
  " / ",
@@ -1934,7 +2958,7 @@ function ContextPanel({ usage, title = "Context", defaultExpanded = true, classN
1934
2958
  }),
1935
2959
  /* @__PURE__ */ jsx("div", {
1936
2960
  role: "meter",
1937
- "aria-label": "Context used",
2961
+ "aria-label": labels.contextUsed,
1938
2962
  "aria-valuemin": 0,
1939
2963
  "aria-valuemax": usage.limit,
1940
2964
  "aria-valuenow": used,
@@ -1956,7 +2980,7 @@ function ContextPanel({ usage, title = "Context", defaultExpanded = true, classN
1956
2980
  }, c.id)), /* @__PURE__ */ jsx(CategoryRow, {
1957
2981
  category: {
1958
2982
  id: "free",
1959
- label: "Free space",
2983
+ label: labels.freeSpace,
1960
2984
  tokens: free,
1961
2985
  color: FREE_COLOR
1962
2986
  },
@@ -1987,7 +3011,7 @@ function CategoryRow({ category, share }) {
1987
3011
  }) })
1988
3012
  }) : category.label;
1989
3013
  return /* @__PURE__ */ jsxs("li", {
1990
- className: "flex items-center gap-2.5 text-sm",
3014
+ className: "flex items-center gap-2.5 text-(length:--chat-ui-font-size) leading-5",
1991
3015
  children: [
1992
3016
  /* @__PURE__ */ jsx("span", {
1993
3017
  "aria-hidden": true,
@@ -2013,16 +3037,16 @@ function GroupList({ group }) {
2013
3037
  return /* @__PURE__ */ jsx(Disclosure, {
2014
3038
  summary: group.label,
2015
3039
  trailing: /* @__PURE__ */ jsx("span", {
2016
- className: "text-sm text-chat-subtle tabular-nums",
3040
+ className: "text-(length:--chat-ui-font-size) leading-5 text-chat-subtle tabular-nums",
2017
3041
  children: group.items.length
2018
3042
  }),
2019
3043
  indicator: "chevron",
2020
- summaryClassName: "text-sm",
3044
+ summaryClassName: "text-(length:--chat-ui-font-size) leading-5",
2021
3045
  panelClassName: "pt-1.5",
2022
3046
  children: /* @__PURE__ */ jsx("ul", {
2023
3047
  className: "max-h-72 space-y-1 overflow-y-auto pl-6 [scrollbar-color:var(--chat-border-strong)_transparent] [scrollbar-width:thin]",
2024
3048
  children: group.items.map((item) => /* @__PURE__ */ jsxs("li", {
2025
- className: "flex items-center gap-2 text-sm text-chat-muted",
3049
+ className: "flex items-center gap-2 text-(length:--chat-ui-font-size) leading-5 text-chat-muted",
2026
3050
  children: [/* @__PURE__ */ jsx("span", {
2027
3051
  className: "min-w-0 flex-1 truncate",
2028
3052
  children: item.label
@@ -2041,7 +3065,9 @@ function formatShare(fraction) {
2041
3065
  /** Touch keyboards have no easy Shift+Enter, so there Enter adds a line and the button sends. */
2042
3066
  const coarsePointer = () => typeof window !== "undefined" && window.matchMedia?.("(pointer: coarse)").matches;
2043
3067
  /** 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 }) {
3068
+ 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 }) {
3069
+ const icons = useIcons();
3070
+ const labels = useLabels();
2045
3071
  const [own, setOwn] = useState("");
2046
3072
  const text = value ?? own;
2047
3073
  const setText = (next) => {
@@ -2114,18 +3140,18 @@ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
2114
3140
  };
2115
3141
  const compact = variant === "compact";
2116
3142
  const action = busy ? /* @__PURE__ */ jsx(IconButton, {
2117
- label: "Stop",
3143
+ label: labels.stop,
2118
3144
  variant: "primary",
2119
3145
  onPress: onStop,
2120
3146
  className: "rounded-chat-md [&_svg]:size-3.5 [&_svg]:fill-current",
2121
- children: /* @__PURE__ */ jsx(Square, {})
3147
+ children: icons.stop
2122
3148
  }) : /* @__PURE__ */ jsx(IconButton, {
2123
- label: "Send",
3149
+ label: labels.send,
2124
3150
  variant: "primary",
2125
3151
  onPress: submit,
2126
3152
  isDisabled: !canSend,
2127
3153
  className: "rounded-chat-md",
2128
- children: /* @__PURE__ */ jsx(ArrowUp, {})
3154
+ children: icons.send
2129
3155
  });
2130
3156
  return /* @__PURE__ */ jsxs("div", {
2131
3157
  className: clsx("flex flex-col rounded-[1.25rem] border border-chat-border bg-chat-surface font-chat-sans shadow-chat-popover", "focus-within:border-chat-border-strong", dragging && "border-chat-accent! border-dashed", compact ? "gap-1 p-2" : "gap-2 p-3", className),
@@ -2147,8 +3173,8 @@ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
2147
3173
  onChange: (e) => setText(e.target.value),
2148
3174
  onKeyDown,
2149
3175
  onPaste,
2150
- placeholder,
2151
- "aria-label": "Message",
3176
+ placeholder: placeholder ?? labels.replyPlaceholder,
3177
+ "aria-label": labels.message,
2152
3178
  disabled: isDisabled,
2153
3179
  autoFocus,
2154
3180
  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,10 +3198,12 @@ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
2172
3198
  });
2173
3199
  }
2174
3200
  /** The `+` button that opens the attach menu above the composer. */
2175
- function AttachButton({ children, label = "Add files and more" }) {
3201
+ function AttachButton({ children, label }) {
3202
+ const icons = useIcons();
3203
+ const labels = useLabels();
2176
3204
  return /* @__PURE__ */ jsxs(MenuTrigger, { children: [/* @__PURE__ */ jsx(IconButton, {
2177
- label,
2178
- children: /* @__PURE__ */ jsx(Plus, {})
3205
+ label: label ?? labels.addFiles,
3206
+ children: icons.attach
2179
3207
  }), /* @__PURE__ */ jsx(Popover, {
2180
3208
  placement: "top start",
2181
3209
  children
@@ -2183,7 +3211,9 @@ function AttachButton({ children, label = "Add files and more" }) {
2183
3211
  }
2184
3212
  // src/components/model-picker.tsx
2185
3213
  /** "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 }) {
3214
+ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, effortNote, footnote, className }) {
3215
+ const icons = useIcons();
3216
+ const labels = useLabels();
2187
3217
  const current = models.find((m) => m.id === model);
2188
3218
  const efforts = current?.efforts ?? [];
2189
3219
  const currentEffort = efforts.find((e) => e.id === (effort ?? current?.defaultEffort));
@@ -2202,7 +3232,7 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
2202
3232
  };
2203
3233
  return /* @__PURE__ */ jsxs(MenuTrigger, { children: [/* @__PURE__ */ jsxs(Button, {
2204
3234
  size: "sm",
2205
- "aria-label": `Model: ${current?.label ?? model}`,
3235
+ "aria-label": labels.modelButton(current?.label ?? model),
2206
3236
  className: clsx("text-chat-muted data-hovered:text-chat-fg", className),
2207
3237
  children: [
2208
3238
  /* @__PURE__ */ jsx("span", {
@@ -2213,16 +3243,16 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
2213
3243
  className: "chat-picker-detail text-chat-muted",
2214
3244
  children: currentEffort.label
2215
3245
  }),
2216
- /* @__PURE__ */ jsx(ChevronDown, {
2217
- "aria-hidden": true,
2218
- className: "size-3.5"
3246
+ /* @__PURE__ */ jsx(IconSlot, {
3247
+ className: "[&_svg]:size-3.5",
3248
+ children: icons.dropdown
2219
3249
  })
2220
3250
  ]
2221
3251
  }), /* @__PURE__ */ jsxs(Popover, {
2222
3252
  placement: "top end",
2223
3253
  className: "w-72",
2224
3254
  children: [/* @__PURE__ */ jsxs(Menu, {
2225
- "aria-label": "Model",
3255
+ "aria-label": labels.model,
2226
3256
  children: [
2227
3257
  /* @__PURE__ */ jsx(MenuSection, {
2228
3258
  selectionMode: "single",
@@ -2234,12 +3264,12 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
2234
3264
  efforts.length > 0 && onEffortChange && /* @__PURE__ */ jsxs(SubmenuTrigger, { children: [/* @__PURE__ */ jsx(MenuItem, {
2235
3265
  id: "__effort",
2236
3266
  detail: currentEffort?.label,
2237
- children: "Effort"
3267
+ children: labels.effort
2238
3268
  }), /* @__PURE__ */ jsxs(Popover, {
2239
3269
  placement: "end top",
2240
3270
  className: "w-80",
2241
- children: [/* @__PURE__ */ jsx(MenuNote, { children: effortNote }), /* @__PURE__ */ jsx(Menu, {
2242
- "aria-label": "Effort",
3271
+ children: [/* @__PURE__ */ jsx(MenuNote, { children: effortNote ?? labels.effortNote }), /* @__PURE__ */ jsx(Menu, {
3272
+ "aria-label": labels.effort,
2243
3273
  selectionMode: "single",
2244
3274
  selectedKeys: currentEffort ? [currentEffort.id] : [],
2245
3275
  onSelectionChange: (keys) => {
@@ -2254,10 +3284,10 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
2254
3284
  className: "flex items-center gap-2",
2255
3285
  children: [
2256
3286
  e.label,
2257
- e.recommended && /* @__PURE__ */ jsx(Badge, { children: "Recommended" }),
3287
+ e.recommended && /* @__PURE__ */ jsx(Badge, { children: labels.recommended }),
2258
3288
  e.warning && /* @__PURE__ */ jsx(Badge, {
2259
3289
  tone: "warning",
2260
- icon: /* @__PURE__ */ jsx(AlertTriangle, {}),
3290
+ icon: icons.warning,
2261
3291
  children: e.warning
2262
3292
  })
2263
3293
  ]
@@ -2267,12 +3297,12 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
2267
3297
  })] }),
2268
3298
  more.length > 0 && /* @__PURE__ */ jsxs(SubmenuTrigger, { children: [/* @__PURE__ */ jsx(MenuItem, {
2269
3299
  id: "__more",
2270
- children: "More models"
3300
+ children: labels.moreModels
2271
3301
  }), /* @__PURE__ */ jsx(Popover, {
2272
3302
  placement: "end top",
2273
3303
  className: "w-72",
2274
3304
  children: /* @__PURE__ */ jsx(Menu, {
2275
- "aria-label": "More models",
3305
+ "aria-label": labels.moreModels,
2276
3306
  selectionMode: "single",
2277
3307
  selectedKeys: [model],
2278
3308
  onSelectionChange: pick,
@@ -2281,7 +3311,7 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
2281
3311
  })] })
2282
3312
  ]
2283
3313
  }), footnote && /* @__PURE__ */ jsx(MenuNote, {
2284
- className: "border-t border-chat-popover-border text-xs",
3314
+ className: "border-t border-chat-popover-border text-(length:--chat-ui-font-size-sm) leading-4",
2285
3315
  children: footnote
2286
3316
  })]
2287
3317
  })] });
@@ -2293,10 +3323,12 @@ const STICK_SLOP = 48;
2293
3323
  * The scrolling conversation. It follows a streaming reply while the reader is at the bottom and leaves
2294
3324
  * them alone once they scroll up, with a button to jump back. Sending a message always jumps down.
2295
3325
  */
2296
- function MessageList({ messages, onRetry, onEdit, onFeedback, renderPart, footer, className }) {
3326
+ function MessageList({ messages, onRetry, onEdit, onFeedback, renderPart, header, footer, start = "bottom", className }) {
3327
+ const icons = useIcons();
2297
3328
  const scroller = useRef(null);
2298
3329
  const content = useRef(null);
2299
- const stuck = useRef(true);
3330
+ const stuck = useRef(start === "bottom");
3331
+ const labels = useLabels();
2300
3332
  const [atBottom, setAtBottom] = useState(true);
2301
3333
  const toBottom = useCallback((behavior = "auto") => {
2302
3334
  const el = scroller.current;
@@ -2322,7 +3354,13 @@ function MessageList({ messages, onRetry, onEdit, onFeedback, renderPart, footer
2322
3354
  return () => observer.disconnect();
2323
3355
  }, [toBottom]);
2324
3356
  const lastUser = messages.findLast((m) => m.role === "user")?.id;
3357
+ const jumpedFor = useRef(start === "top" ? lastUser : null);
2325
3358
  useLayoutEffect(() => {
3359
+ if (lastUser === jumpedFor.current) {
3360
+ onScroll();
3361
+ return;
3362
+ }
3363
+ jumpedFor.current = lastUser;
2326
3364
  stuck.current = true;
2327
3365
  setAtBottom(true);
2328
3366
  toBottom();
@@ -2336,11 +3374,11 @@ function MessageList({ messages, onRetry, onEdit, onFeedback, renderPart, footer
2336
3374
  children: /* @__PURE__ */ jsxs("div", {
2337
3375
  ref: content,
2338
3376
  className: "flex min-h-full flex-col",
2339
- children: [/* @__PURE__ */ jsx("div", {
3377
+ children: [/* @__PURE__ */ jsxs("div", {
2340
3378
  role: "log",
2341
- "aria-label": "Conversation",
3379
+ "aria-label": labels.conversation,
2342
3380
  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, {
3381
+ children: [header, messages.map((m) => m.role === "user" ? /* @__PURE__ */ jsx(UserMessage, {
2344
3382
  message: m,
2345
3383
  ...onEdit && { onEdit: () => onEdit(m.id) }
2346
3384
  }, m.id) : /* @__PURE__ */ jsx(AssistantMessage, {
@@ -2348,27 +3386,28 @@ function MessageList({ messages, onRetry, onEdit, onFeedback, renderPart, footer
2348
3386
  ...onRetry && { onRetry: () => onRetry(m.id) },
2349
3387
  ...onFeedback && { onFeedback: (f) => onFeedback(m.id, f) },
2350
3388
  ...renderPart && { renderPart }
2351
- }, m.id))
3389
+ }, m.id))]
2352
3390
  }), footer && /* @__PURE__ */ jsx("div", {
2353
3391
  className: "sticky bottom-0",
2354
3392
  children: footer
2355
3393
  })]
2356
3394
  })
2357
3395
  }), !atBottom && /* @__PURE__ */ jsx(IconButton, {
2358
- label: "Scroll to bottom",
3396
+ label: labels.scrollToBottom,
2359
3397
  variant: "subtle",
2360
3398
  onPress: () => {
2361
3399
  stuck.current = true;
2362
3400
  toBottom("smooth");
2363
3401
  },
2364
3402
  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, {})
3403
+ children: icons.scrollToBottom
2366
3404
  })]
2367
3405
  });
2368
3406
  }
2369
3407
  // src/components/chat-header.tsx
2370
3408
  /** The bar above a chat: breadcrumb and title with its menu, actions on the right. */
2371
3409
  function ChatHeader({ title, menu, leading, actions, className }) {
3410
+ const icons = useIcons();
2372
3411
  return /* @__PURE__ */ jsxs("header", {
2373
3412
  className: clsx("flex h-12 shrink-0 items-center gap-1 px-3 font-chat-sans", className),
2374
3413
  children: [
@@ -2379,15 +3418,15 @@ function ChatHeader({ title, menu, leading, actions, className }) {
2379
3418
  children: [/* @__PURE__ */ jsx("span", {
2380
3419
  className: "truncate",
2381
3420
  children: title
2382
- }), /* @__PURE__ */ jsx(ChevronDown, {
2383
- "aria-hidden": true,
2384
- className: "size-3.5 shrink-0 text-chat-muted"
3421
+ }), /* @__PURE__ */ jsx(IconSlot, {
3422
+ className: "text-chat-muted [&_svg]:size-3.5",
3423
+ children: icons.dropdown
2385
3424
  })]
2386
3425
  }), /* @__PURE__ */ jsx(Popover, {
2387
3426
  placement: "bottom start",
2388
3427
  children: menu
2389
3428
  })] }) : /* @__PURE__ */ jsx("h1", {
2390
- className: "min-w-0 truncate px-2 text-sm text-chat-fg",
3429
+ className: "min-w-0 truncate px-2 text-(length:--chat-ui-font-size) leading-5 text-chat-fg",
2391
3430
  children: title
2392
3431
  }),
2393
3432
  /* @__PURE__ */ jsx("div", {
@@ -2399,6 +3438,8 @@ function ChatHeader({ title, menu, leading, actions, className }) {
2399
3438
  }
2400
3439
  /** Pin, Rename, Add to project (searchable), Share, Delete. Each shows only when its handler is set. */
2401
3440
  function ChatTitleMenu({ pinned, onPin, onRename, projects, onAddToProject, onCreateProject, onShare, onDelete, children }) {
3441
+ const icons = useIcons();
3442
+ const labels = useLabels();
2402
3443
  const act = (key) => {
2403
3444
  if (key === "pin") onPin?.();
2404
3445
  else if (key === "rename") onRename?.();
@@ -2406,39 +3447,39 @@ function ChatTitleMenu({ pinned, onPin, onRename, projects, onAddToProject, onCr
2406
3447
  else if (key === "delete") onDelete?.();
2407
3448
  };
2408
3449
  return /* @__PURE__ */ jsxs(Menu, {
2409
- "aria-label": "Chat actions",
3450
+ "aria-label": labels.chatActions,
2410
3451
  className: "min-w-44",
2411
3452
  onAction: act,
2412
3453
  children: [
2413
3454
  onPin && /* @__PURE__ */ jsx(MenuItem, {
2414
3455
  id: "pin",
2415
- icon: pinned ? /* @__PURE__ */ jsx(PinOff, {}) : /* @__PURE__ */ jsx(Pin, {}),
3456
+ icon: pinned ? icons.unpin : icons.pin,
2416
3457
  shortcut: "P",
2417
- children: pinned ? "Unpin" : "Pin"
3458
+ children: pinned ? labels.unpin : labels.pin
2418
3459
  }),
2419
3460
  onRename && /* @__PURE__ */ jsx(MenuItem, {
2420
3461
  id: "rename",
2421
- icon: /* @__PURE__ */ jsx(Pencil, {}),
3462
+ icon: icons.rename,
2422
3463
  shortcut: "R",
2423
- children: "Rename"
3464
+ children: labels.rename
2424
3465
  }),
2425
3466
  onAddToProject && /* @__PURE__ */ jsxs(SubmenuTrigger, { children: [/* @__PURE__ */ jsx(MenuItem, {
2426
3467
  id: "project",
2427
- icon: /* @__PURE__ */ jsx(FolderPlus, {}),
2428
- children: "Add to project"
3468
+ icon: icons.addToProject,
3469
+ children: labels.addToProject
2429
3470
  }), /* @__PURE__ */ jsx(Popover, {
2430
3471
  placement: "end top",
2431
3472
  className: "w-60",
2432
3473
  children: /* @__PURE__ */ jsx(MenuSearch, {
2433
- placeholder: "Search or create a project",
3474
+ placeholder: labels.searchProjects,
2434
3475
  children: /* @__PURE__ */ jsxs(Menu, {
2435
- "aria-label": "Projects",
3476
+ "aria-label": labels.projects,
2436
3477
  className: "max-h-72",
2437
3478
  onAction: (key) => key === "__new" ? onCreateProject?.() : onAddToProject(String(key)),
2438
3479
  children: [onCreateProject && /* @__PURE__ */ jsx(MenuItem, {
2439
3480
  id: "__new",
2440
- icon: /* @__PURE__ */ jsx(Plus, {}),
2441
- children: "New project"
3481
+ icon: icons.add,
3482
+ children: labels.newProject
2442
3483
  }), (projects ?? []).map((p) => /* @__PURE__ */ jsx(MenuItem, {
2443
3484
  id: p.id,
2444
3485
  children: p.name
@@ -2448,28 +3489,50 @@ function ChatTitleMenu({ pinned, onPin, onRename, projects, onAddToProject, onCr
2448
3489
  })] }),
2449
3490
  onShare && /* @__PURE__ */ jsx(MenuItem, {
2450
3491
  id: "share",
2451
- icon: /* @__PURE__ */ jsx(Share, {}),
2452
- children: "Share chat"
3492
+ icon: icons.share,
3493
+ children: labels.shareChat
2453
3494
  }),
2454
3495
  children,
2455
3496
  onDelete && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MenuSeparator, {}), /* @__PURE__ */ jsx(MenuItem, {
2456
3497
  id: "delete",
2457
- icon: /* @__PURE__ */ jsx(Trash2, {}),
3498
+ icon: icons.delete,
2458
3499
  shortcut: "D",
2459
3500
  tone: "danger",
2460
- children: "Delete"
3501
+ children: labels.delete
2461
3502
  })] })
2462
3503
  ]
2463
3504
  });
2464
3505
  }
3506
+ // src/components/chat-notice.tsx
3507
+ /** A bordered note in the conversation column: "Shared by Alex. This is a copy of a chat…" with actions under it. */
3508
+ function ChatNotice({ children, actions, icon, className }) {
3509
+ const icons = useIcons();
3510
+ const shown = icon === void 0 ? icons.info : icon;
3511
+ return /* @__PURE__ */ jsxs("div", {
3512
+ role: "note",
3513
+ 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),
3514
+ children: [shown && /* @__PURE__ */ jsx("span", {
3515
+ className: "mt-0.5 shrink-0 text-chat-muted [&_svg]:size-4",
3516
+ children: shown
3517
+ }), /* @__PURE__ */ jsxs("div", {
3518
+ className: "min-w-0 flex-1",
3519
+ children: [/* @__PURE__ */ jsx("div", { children }), actions && /* @__PURE__ */ jsx("div", {
3520
+ className: "mt-2.5 flex flex-wrap gap-2",
3521
+ children: actions
3522
+ })]
3523
+ })]
3524
+ });
3525
+ }
2465
3526
  // src/components/chat-list.tsx
2466
3527
  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");
3528
+ 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
3529
  /**
2469
3530
  * The chat picker: sections of chats and projects, collapsible, with an active row, running and unread
2470
3531
  * markers and a per-row menu. Usable on its own (in a host's own sidebar) or inside `Sidebar`.
2471
3532
  */
2472
3533
  function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }) {
3534
+ const icons = useIcons();
3535
+ const labels = useLabels();
2473
3536
  const row = (chat, nested = false) => /* @__PURE__ */ jsx(ChatRow, {
2474
3537
  chat,
2475
3538
  active: chat.id === activeId,
@@ -2483,13 +3546,13 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
2483
3546
  row
2484
3547
  }, item.id) : row(item));
2485
3548
  return /* @__PURE__ */ jsx("nav", {
2486
- "aria-label": props["aria-label"] ?? "Chats",
3549
+ "aria-label": props["aria-label"] ?? labels.chats,
2487
3550
  className: clsx("flex flex-col gap-3", className),
2488
3551
  children: sections.map((section) => section.title ? /* @__PURE__ */ jsxs(Disclosure$1, {
2489
3552
  defaultExpanded: section.defaultExpanded ?? true,
2490
3553
  className: "group/section",
2491
3554
  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",
3555
+ 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
3556
  children: [
2494
3557
  /* @__PURE__ */ jsxs(Button$1, {
2495
3558
  slot: "trigger",
@@ -2497,9 +3560,9 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
2497
3560
  children: [/* @__PURE__ */ jsx("span", {
2498
3561
  className: "truncate",
2499
3562
  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"
3563
+ }), /* @__PURE__ */ jsx(IconSlot, {
3564
+ 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",
3565
+ children: icons.expand
2503
3566
  })]
2504
3567
  }),
2505
3568
  section.badge,
@@ -2514,7 +3577,7 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
2514
3577
  }), section.onViewAll && /* @__PURE__ */ jsx(Button$1, {
2515
3578
  onPress: section.onViewAll,
2516
3579
  className: clsx(rowClass, "text-chat-muted"),
2517
- children: "View all"
3580
+ children: labels.viewAll
2518
3581
  })] })]
2519
3582
  }, section.id) : /* @__PURE__ */ jsx("ul", {
2520
3583
  className: "flex flex-col gap-px",
@@ -2523,16 +3586,18 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
2523
3586
  });
2524
3587
  }
2525
3588
  function ChatRow({ chat, active, nested, onSelect, menu }) {
3589
+ const icons = useIcons();
2526
3590
  const [menuOpen, setMenuOpen] = useState(false);
2527
3591
  const { dismiss } = useSidebar();
3592
+ const labels = useLabels();
2528
3593
  const markerClass = "size-2 shrink-0 rounded-full";
2529
3594
  const marker = chat.running ? /* @__PURE__ */ jsx("span", {
2530
3595
  role: "img",
2531
- "aria-label": "Running",
3596
+ "aria-label": labels.running,
2532
3597
  className: clsx(markerClass, "animate-pulse bg-chat-accent motion-reduce:animate-none")
2533
3598
  }) : chat.unread ? /* @__PURE__ */ jsx("span", {
2534
3599
  role: "img",
2535
- "aria-label": "Unread",
3600
+ "aria-label": labels.unread,
2536
3601
  className: clsx(markerClass, "bg-chat-check")
2537
3602
  }) : null;
2538
3603
  const content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
@@ -2557,11 +3622,11 @@ function ChatRow({ chat, active, nested, onSelect, menu }) {
2557
3622
  isOpen: menuOpen,
2558
3623
  onOpenChange: setMenuOpen,
2559
3624
  children: [/* @__PURE__ */ jsx(IconButton, {
2560
- label: `More options for ${chat.title}`,
3625
+ label: labels.moreOptionsFor(chat.title),
2561
3626
  size: "sm",
2562
3627
  tooltip: false,
2563
3628
  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, {})
3629
+ children: icons.more
2565
3630
  }), /* @__PURE__ */ jsx(Popover, {
2566
3631
  placement: "bottom start",
2567
3632
  children: menu
@@ -2570,6 +3635,8 @@ function ChatRow({ chat, active, nested, onSelect, menu }) {
2570
3635
  });
2571
3636
  }
2572
3637
  function ProjectRow({ project, activeId, row }) {
3638
+ const icons = useIcons();
3639
+ const labels = useLabels();
2573
3640
  const holdsActive = project.chats.some((c) => c.id === activeId);
2574
3641
  return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Disclosure$1, {
2575
3642
  defaultExpanded: project.defaultExpanded ?? holdsActive,
@@ -2579,18 +3646,15 @@ function ProjectRow({ project, activeId, row }) {
2579
3646
  slot: "trigger",
2580
3647
  className: rowClass,
2581
3648
  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"
3649
+ /* @__PURE__ */ jsx(IconSlot, {
3650
+ className: "text-chat-muted [&_svg]:size-4",
3651
+ children: isExpanded ? icons.folderOpen : icons.folder
2588
3652
  }),
2589
3653
  /* @__PURE__ */ jsx("span", {
2590
3654
  className: "min-w-0 flex-1 truncate",
2591
3655
  children: project.name
2592
3656
  }),
2593
- !isExpanded && project.chats.some((c) => c.unread || c.running) && /* @__PURE__ */ jsx(Badge, { children: "New" })
3657
+ !isExpanded && project.chats.some((c) => c.unread || c.running) && /* @__PURE__ */ jsx(Badge, { children: labels.new })
2594
3658
  ]
2595
3659
  })
2596
3660
  }), /* @__PURE__ */ jsx(DisclosurePanel, { children: /* @__PURE__ */ jsx("ul", {
@@ -2600,7 +3664,11 @@ function ProjectRow({ project, activeId, row }) {
2600
3664
  }) });
2601
3665
  }
2602
3666
  // src/views/chat-view.tsx
2603
- /** Column defaults per density; `contentWidth` / `contentPadding` override them. */
3667
+ /**
3668
+ * Column defaults per density; `contentWidth` / `contentPadding` override them. Only the message size changes
3669
+ * with density: UI text is the same in all three. A theme can set the other text-size variables per density
3670
+ * with `[data-chat-density="panel"] { --chat-ui-font-size: … }`.
3671
+ */
2604
3672
  const densityVars = {
2605
3673
  full: {},
2606
3674
  panel: {
@@ -2615,14 +3683,20 @@ const densityVars = {
2615
3683
  }
2616
3684
  };
2617
3685
  /** 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 }) {
3686
+ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove, onRetry, onEdit, onFeedback, renderPart, widgets, header, composer, emptyState, disclaimer, density = "full", contentWidth, contentPadding, className }) {
2619
3687
  const style = {
2620
3688
  ...densityVars[density],
2621
3689
  ...contentWidth && { "--chat-content-width": contentWidth },
2622
3690
  ...contentPadding && { "--chat-content-padding": contentPadding }
2623
3691
  };
3692
+ const host = useMemo(() => ({
3693
+ onPrompt: onSend,
3694
+ onOpenLink: openInNewTab,
3695
+ ...widgets
3696
+ }), [onSend, widgets]);
2624
3697
  const empty = messages.length === 0;
2625
3698
  const [body, short] = useShortPane();
3699
+ const labels = useLabels();
2626
3700
  const pending = onApprove && approvals?.[0];
2627
3701
  const box = (variant) => /* @__PURE__ */ jsx(Composer, {
2628
3702
  variant: density === "inline" || short && !empty ? "compact" : variant,
@@ -2630,7 +3704,7 @@ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove
2630
3704
  onSubmit: onSend,
2631
3705
  ...onStop && { onStop },
2632
3706
  ...composer,
2633
- ...pending && { placeholder: "Or reply directly…" }
3707
+ ...pending && { placeholder: labels.replyDirectly }
2634
3708
  });
2635
3709
  return /* @__PURE__ */ jsxs("div", {
2636
3710
  "data-chat-density": density,
@@ -2645,28 +3719,31 @@ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove
2645
3719
  className: "w-full max-w-(--chat-content-width)",
2646
3720
  children: emptyState ? emptyState(box("centered")) : box("centered")
2647
3721
  })
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
- ]
3722
+ }) : /* @__PURE__ */ jsx(WidgetHostProvider, {
3723
+ host,
3724
+ children: /* @__PURE__ */ jsx(MessageList, {
3725
+ messages,
3726
+ ...onRetry && { onRetry },
3727
+ ...onEdit && { onEdit },
3728
+ ...onFeedback && { onFeedback },
3729
+ ...renderPart && { renderPart },
3730
+ footer: /* @__PURE__ */ jsx("div", {
3731
+ className: clsx("bg-linear-to-t from-chat-bg from-70% to-transparent px-(--chat-gutter)", short ? "pt-2" : "pt-4"),
3732
+ children: /* @__PURE__ */ jsxs("div", {
3733
+ className: "mx-auto w-full max-w-(--chat-content-width)",
3734
+ children: [
3735
+ pending && /* @__PURE__ */ jsx(ApprovalCard, {
3736
+ approval: pending,
3737
+ onRespond: onApprove,
3738
+ className: "mb-2"
3739
+ }, pending.part.id),
3740
+ box("docked"),
3741
+ /* @__PURE__ */ jsx("div", {
3742
+ 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"),
3743
+ children: disclaimer && !short && /* @__PURE__ */ jsx("span", { children: disclaimer })
3744
+ })
3745
+ ]
3746
+ })
2670
3747
  })
2671
3748
  })
2672
3749
  })
@@ -2703,6 +3780,51 @@ function EmptyState({ greeting, icon, children, footer }) {
2703
3780
  ]
2704
3781
  });
2705
3782
  }
3783
+ // src/views/chat-transcript.tsx
3784
+ /**
3785
+ * A conversation to read, not to continue: a shared chat, an export, a log. Same messages as ChatView,
3786
+ * with no composer, approvals, retry, edit or feedback. Copy stays.
3787
+ */
3788
+ function ChatTranscript({ messages, renderPart, widgets, header, notice, footer, start = "top", density = "full", contentWidth, contentPadding, className }) {
3789
+ const style = {
3790
+ ...densityVars[density],
3791
+ ...contentWidth && { "--chat-content-width": contentWidth },
3792
+ ...contentPadding && { "--chat-content-padding": contentPadding }
3793
+ };
3794
+ const host = useMemo(() => {
3795
+ const { allow, readResource } = { ...widgets };
3796
+ const onOpenLink = widgets && "onOpenLink" in widgets ? widgets.onOpenLink : openInNewTab;
3797
+ return {
3798
+ ...allow && { allow },
3799
+ ...onOpenLink && { onOpenLink },
3800
+ ...readResource && { readResource }
3801
+ };
3802
+ }, [widgets]);
3803
+ return /* @__PURE__ */ jsxs("div", {
3804
+ "data-chat-density": density,
3805
+ style,
3806
+ className: clsx("chat-view flex h-full min-h-0 flex-col bg-chat-bg text-chat-fg", className),
3807
+ children: [header, /* @__PURE__ */ jsx("div", {
3808
+ className: "chat-view-body flex min-h-0 flex-1 flex-col",
3809
+ children: /* @__PURE__ */ jsx(WidgetHostProvider, {
3810
+ host,
3811
+ children: /* @__PURE__ */ jsx(MessageList, {
3812
+ messages,
3813
+ start,
3814
+ ...notice && { header: notice },
3815
+ ...renderPart && { renderPart },
3816
+ ...footer && { footer: /* @__PURE__ */ jsx("div", {
3817
+ 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))]",
3818
+ children: /* @__PURE__ */ jsx("div", {
3819
+ className: "mx-auto w-full max-w-(--chat-content-width)",
3820
+ children: footer
3821
+ })
3822
+ }) }
3823
+ })
3824
+ })
3825
+ })]
3826
+ });
3827
+ }
2706
3828
  // src/views/app-shell.tsx
2707
3829
  /**
2708
3830
  * A page with a sidebar: pinned and collapsible to an icon rail from tablet width up, a drawer over the
@@ -2727,6 +3849,7 @@ function AppShell({ sidebar, children, defaultCollapsed, className }) {
2727
3849
  toggle,
2728
3850
  dismiss
2729
3851
  ]);
3852
+ const labels = useLabels();
2730
3853
  const inDrawer = useMemo(() => ({
2731
3854
  ...pinned,
2732
3855
  collapsed: false
@@ -2745,7 +3868,7 @@ function AppShell({ sidebar, children, defaultCollapsed, className }) {
2745
3868
  }), drawer && /* @__PURE__ */ jsx(Drawer, {
2746
3869
  isOpen: drawerOpen,
2747
3870
  onOpenChange: setDrawerOpen,
2748
- "aria-label": "Sidebar",
3871
+ "aria-label": labels.sidebar,
2749
3872
  children: /* @__PURE__ */ jsx(SidebarContext.Provider, {
2750
3873
  value: inDrawer,
2751
3874
  children: sidebar
@@ -2755,13 +3878,16 @@ function AppShell({ sidebar, children, defaultCollapsed, className }) {
2755
3878
  }
2756
3879
  /** Opens the drawer on phones, collapses / expands the pinned sidebar elsewhere. */
2757
3880
  function SidebarToggle({ drawerOnly = false, className }) {
3881
+ const icons = useIcons();
2758
3882
  const { drawer, drawerOpen, collapsed, toggle } = useSidebar();
3883
+ const labels = useLabels();
2759
3884
  if (drawerOnly && !drawer) return null;
3885
+ const label = drawer ? drawerOpen ? labels.closeSidebar : labels.openSidebar : collapsed ? labels.expandSidebar : labels.collapseSidebar;
2760
3886
  return /* @__PURE__ */ jsx(IconButton, {
2761
- label: drawer ? drawerOpen ? "Close sidebar" : "Open sidebar" : collapsed ? "Expand sidebar" : "Collapse sidebar",
3887
+ label,
2762
3888
  onPress: toggle,
2763
3889
  className,
2764
- children: /* @__PURE__ */ jsx(PanelLeft, {})
3890
+ children: icons.sidebar
2765
3891
  });
2766
3892
  }
2767
3893
  // src/views/sidebar.tsx
@@ -2801,7 +3927,7 @@ function Sidebar({ brand, headerActions, nav, children, footer, className }) {
2801
3927
  /** A nav row: icon and label; just the icon with a tooltip when collapsed. Closes the drawer when picked. */
2802
3928
  function SidebarItem({ icon, label, onPress, href, active, shortcut }) {
2803
3929
  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");
3930
+ 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
3931
  const press = () => {
2806
3932
  onPress?.();
2807
3933
  dismiss();
@@ -2816,7 +3942,7 @@ function SidebarItem({ icon, label, onPress, href, active, shortcut }) {
2816
3942
  children: label
2817
3943
  }),
2818
3944
  !collapsed && shortcut && /* @__PURE__ */ jsx("kbd", {
2819
- className: "font-chat-sans text-xs text-chat-subtle opacity-0 group-data-hovered:opacity-100",
3945
+ className: "font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle opacity-0 group-data-hovered:opacity-100",
2820
3946
  children: shortcut
2821
3947
  })
2822
3948
  ] });
@@ -2842,6 +3968,7 @@ function SidebarItem({ icon, label, onPress, href, active, shortcut }) {
2842
3968
  const initials = (name) => name.split(/\s+/).slice(0, 2).map((w) => w[0]?.toUpperCase() ?? "").join("");
2843
3969
  /** The account row at the bottom: avatar, name, plan, and a menu. */
2844
3970
  function SidebarUser({ name, detail, avatarUrl, menu }) {
3971
+ const icons = useIcons();
2845
3972
  const { collapsed } = useSidebar();
2846
3973
  const avatar = avatarUrl ? /* @__PURE__ */ jsx("img", {
2847
3974
  src: avatarUrl,
@@ -2849,7 +3976,7 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
2849
3976
  className: "size-8 shrink-0 rounded-full object-cover"
2850
3977
  }) : /* @__PURE__ */ jsx("span", {
2851
3978
  "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",
3979
+ 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
3980
  children: initials(name)
2854
3981
  });
2855
3982
  const button = /* @__PURE__ */ jsxs(Button$1, {
@@ -2858,15 +3985,15 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
2858
3985
  children: [avatar, !collapsed && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
2859
3986
  className: "flex min-w-0 flex-1 flex-col",
2860
3987
  children: [/* @__PURE__ */ jsx("span", {
2861
- className: "truncate text-sm text-chat-fg",
3988
+ className: "truncate text-(length:--chat-ui-font-size) leading-5 text-chat-fg",
2862
3989
  children: name
2863
3990
  }), detail && /* @__PURE__ */ jsx("span", {
2864
- className: "truncate text-xs text-chat-muted",
3991
+ className: "truncate text-(length:--chat-ui-font-size-sm) leading-4 text-chat-muted",
2865
3992
  children: detail
2866
3993
  })]
2867
- }), menu && /* @__PURE__ */ jsx(ChevronsUpDown, {
2868
- "aria-hidden": true,
2869
- className: "size-4 shrink-0 text-chat-muted"
3994
+ }), menu && /* @__PURE__ */ jsx(IconSlot, {
3995
+ className: "text-chat-muted [&_svg]:size-4",
3996
+ children: icons.switcher
2870
3997
  })] })]
2871
3998
  });
2872
3999
  if (!menu) return button;
@@ -2875,4 +4002,4 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
2875
4002
  children: menu
2876
4003
  })] });
2877
4004
  }
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 };
4005
+ export { ActivityGroup, AgentProvider, AgentRegistry, AppShell, ApprovalCard, AssistantMessage, AttachButton, AttachmentCard, AttachmentRow, Badge, Button, Caret, ChatHeader, ChatList, ChatNotice, ChatTheme, ChatTitleMenu, ChatTranscript, ChatView, CodeBlock, Composer, ContextMenu, ContextPanel, DRAWER_QUERY, Disclosure, Drawer, EmptyState, IconButton, Markdown, MarkdownLinksProvider, Menu, MenuItem, MenuNote, MenuSearch, MenuSection, MenuSeparator, MenuTrigger, MessageList, Modal, ModelPicker, Panel, PastedTextDialog, Popover, Sidebar, SidebarItem, SidebarToggle, SidebarUser, SubmenuTrigger, TokenCounter, ToolViewsProvider, Tooltip, UiComponentsProvider, UiMarkupBlock, UiTree, UserMessage, WIDGET_MAX_HEIGHT, WidgetFrame, WidgetHostProvider, WidgetPartView, activityHeading, activityLabel, commandTool, defaultIcons, defaultLabels, defaultToolViews, formatDuration, formatRelativeTime, formatTokens, htmlComponent, intersectAllow, memoryTool, messageText, openInNewTab, readTreeInput, readVisualizeInput, sseTransport, uiMarkupToolView, uiTreeToolView, useAgentContext, useAgentRegistry, useAgentSnapshot, useAgentTool, useAttachments, useChat, useChatTool, useCountUp, useIcons, useLabels, useMediaQuery, useSidebar, useSmoothText, useToolView, useToolViews, useUiAction, useUiMarkupLanguage, useUiState, useWidgetHost, userMessage, webFetchTool, webSearchTool, widgetCsp, widgetToolView };