agentic-ui-kit 0.2.0 → 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
  });
@@ -982,7 +1177,10 @@ function useSmoothText(text, streaming) {
982
1177
  s,
983
1178
  prefersReducedMotion
984
1179
  ]);
985
- useEffect(() => () => cancelAnimationFrame(s.frame), [s]);
1180
+ useEffect(() => () => {
1181
+ cancelAnimationFrame(s.frame);
1182
+ s.frame = 0;
1183
+ }, [s]);
986
1184
  return shown;
987
1185
  }
988
1186
  /** Moves `index` forward to the end of the word it falls in, so words are never cut in half. */
@@ -1009,6 +1207,700 @@ const SidebarContext = createContext({
1009
1207
  dismiss: () => {}
1010
1208
  });
1011
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
+ };
1012
1904
  // src/components/word-fade.ts
1013
1905
  const OPAQUE_TAGS = /* @__PURE__ */ new Set([
1014
1906
  "code",
@@ -1075,45 +1967,96 @@ function textOf(node) {
1075
1967
  if (node.type === "text") return node.value ?? "";
1076
1968
  return (node.children ?? []).map(textOf).join("");
1077
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
+ }
1078
1993
  const components = {
1079
1994
  pre: ({ children }) => /* @__PURE__ */ jsx(Fragment, { children }),
1080
- code: ({ node, className, children }) => {
1081
- const hast = node;
1082
- const language = /language-([\w+#-]+)/.exec(className ?? "")?.[1];
1083
- const multiline = hast?.position && hast.position.start.line !== hast.position.end.line;
1084
- if (!language && !multiline) return /* @__PURE__ */ jsx("code", {
1085
- "data-chat-inline-code": true,
1086
- children
1087
- });
1088
- return /* @__PURE__ */ jsx(CodeBlock, {
1089
- variant: "boxed",
1090
- code: textOf(hast).replace(/\n$/, ""),
1091
- ...language && { language }
1092
- });
1093
- }
1995
+ code: ({ node, className, children }) => /* @__PURE__ */ jsx(Code, {
1996
+ node,
1997
+ className,
1998
+ children
1999
+ })
1094
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
+ }
1095
2008
  /** Message markdown: GFM, KaTeX math ($…$ and $$…$$), Shiki code blocks, smooth fade-in while streaming. */
1096
- function Markdown({ children, streaming = false, className }) {
2009
+ function Markdown({ children, streaming = false, className, partId }) {
1097
2010
  const text = useSmoothText(children, streaming);
1098
2011
  const animating = streaming || text.length < children.length;
1099
2012
  const [streamed, setStreamed] = useState(streaming);
1100
2013
  if (streaming && !streamed) setStreamed(true);
1101
2014
  return /* @__PURE__ */ jsx("div", {
1102
2015
  className: clsx("chat-markdown", streamed && "chat-fade", className),
1103
- children: /* @__PURE__ */ jsx(MarkdownBody, {
1104
- text,
1105
- animating,
1106
- 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
+ })
1107
2024
  })
1108
2025
  });
1109
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
+ }
1110
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]);
1111
2054
  return /* @__PURE__ */ jsx(Streamdown, {
1112
2055
  prefix: PREFIX,
1113
2056
  mode: animating || fade ? "streaming" : "static",
1114
2057
  ...fade && { rehypePlugins: fadePlugins },
1115
2058
  plugins: { math },
1116
- components,
2059
+ components: withLinks,
1117
2060
  controls: false,
1118
2061
  lineNumbers: false,
1119
2062
  linkSafety: { enabled: false },
@@ -1153,32 +2096,31 @@ const useChatTool = useAgentTool;
1153
2096
  /** Streaming or running: the model or a tool is busy with it. */
1154
2097
  const isWorking = (part) => part.status !== "done" && part.status !== "error" && part.status !== "awaiting";
1155
2098
  /** Row label for any activity part. */
1156
- function activityLabel(part, view) {
2099
+ function activityLabel(part, labels, view) {
1157
2100
  if (part.type === "thinking") {
1158
2101
  if (part.title) return part.title;
1159
- if (part.status !== "done") return "Thinking…";
1160
- 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;
1161
2104
  }
1162
- return view?.label?.(part) ?? part.title ?? part.name;
2105
+ return view?.label?.(part, labels) ?? part.title ?? part.name;
1163
2106
  }
1164
2107
  /**
1165
2108
  * Heading of an activity group. While live it follows the latest step. Once done it lists what the tools
1166
2109
  * did, in order ("Ran a command, searched the web"); a group of only thinking shows the first title.
1167
2110
  */
1168
- function activityHeading(parts, views) {
2111
+ function activityHeading(parts, views, labels) {
1169
2112
  const waiting = parts.find((p) => p.type === "tool_call" && p.status === "awaiting");
1170
- 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] });
1171
2114
  if (parts.some(isWorking)) {
1172
2115
  const last = parts.at(-1);
1173
- 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);
1174
2117
  }
1175
2118
  const counts = /* @__PURE__ */ new Map();
1176
2119
  for (const part of parts) if (part.type === "tool_call") counts.set(part.name, (counts.get(part.name) ?? 0) + 1);
1177
- if (counts.size === 0) return parts[0] ? activityLabel(parts[0]) : "";
1178
- const text = [...counts].map(([name, count]) => views[name]?.summary?.(count) ?? `used ${name}`).join(", ");
1179
- 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);
1180
2123
  }
1181
- const plural = (count, one, many) => count === 1 ? one : many(count);
1182
2124
  function recordOf(value) {
1183
2125
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1184
2126
  }
@@ -1187,10 +2129,11 @@ function stringOf(value) {
1187
2129
  }
1188
2130
  /** A shell command. Input `{ command, description? }`; result a string or `{ stdout, stderr, exitCode }`. */
1189
2131
  const commandTool = {
1190
- label: () => "Ran a command",
2132
+ label: (_, labels) => labels.ranCommandLabel,
1191
2133
  hint: (part) => stringOf(recordOf(part.input).description),
1192
- summary: (n) => plural(n, "ran a command", (n) => `ran ${n} commands`),
2134
+ summary: (n, labels) => labels.ranCommands(n),
1193
2135
  Detail: function CommandDetail({ part }) {
2136
+ const labels = useLabels();
1194
2137
  const command = stringOf(recordOf(part.input).command) ?? part.inputJson;
1195
2138
  const result = recordOf(part.result);
1196
2139
  const output = stringOf(part.result) ?? [stringOf(result.stdout), stringOf(result.stderr)].filter(Boolean).join("\n");
@@ -1202,31 +2145,37 @@ const commandTool = {
1202
2145
  code: command
1203
2146
  }),
1204
2147
  (output || part.status === "done") && /* @__PURE__ */ jsx(CodeBlock, {
1205
- label: "Output",
1206
- code: output || "(no output)"
2148
+ label: labels.output,
2149
+ code: output || labels.noOutput
1207
2150
  }),
1208
2151
  part.error && /* @__PURE__ */ jsx("p", {
1209
- className: "text-sm text-chat-danger",
2152
+ className: "text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
1210
2153
  children: part.error
1211
2154
  })
1212
2155
  ]
1213
2156
  });
1214
2157
  }
1215
2158
  };
1216
- /** 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
+ */
1217
2163
  const webSearchTool = {
1218
- label: () => "Searched the web",
2164
+ label: (_, labels) => labels.searchedWebLabel,
1219
2165
  subtitle: (part) => stringOf(recordOf(part.input).query) ?? part.subtitle,
1220
- summary: (n) => plural(n, "searched the web", (n) => `searched the web ${n} times`),
2166
+ summary: (n, labels) => labels.searchedWeb(n),
1221
2167
  Detail: function WebSearchDetail({ part }) {
1222
- 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;
1223
2172
  if (part.error) return /* @__PURE__ */ jsx("p", {
1224
- className: "text-sm text-chat-danger",
2173
+ className: "text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
1225
2174
  children: part.error
1226
2175
  });
1227
2176
  if (!results?.length) return /* @__PURE__ */ jsx("p", {
1228
- className: "text-sm text-chat-muted",
1229
- children: "No results"
2177
+ className: "text-(length:--chat-ui-font-size) leading-5 text-chat-muted",
2178
+ children: labels.noResults
1230
2179
  });
1231
2180
  return /* @__PURE__ */ jsx("ul", {
1232
2181
  className: "-mx-1",
@@ -1234,22 +2183,22 @@ const webSearchTool = {
1234
2183
  href: result.url,
1235
2184
  target: "_blank",
1236
2185
  rel: "noreferrer",
1237
- 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",
1238
2187
  children: [
1239
2188
  result.favicon ? /* @__PURE__ */ jsx("img", {
1240
2189
  src: result.favicon,
1241
2190
  alt: "",
1242
2191
  className: "size-4 shrink-0 rounded-sm"
1243
- }) : /* @__PURE__ */ jsx(Globe, {
1244
- "aria-hidden": true,
1245
- className: "size-4 shrink-0"
2192
+ }) : /* @__PURE__ */ jsx(IconSlot, {
2193
+ className: "[&_svg]:size-4",
2194
+ children: icons.web
1246
2195
  }),
1247
2196
  /* @__PURE__ */ jsx("span", {
1248
2197
  className: "min-w-0 flex-1 truncate",
1249
2198
  children: result.title
1250
2199
  }),
1251
2200
  /* @__PURE__ */ jsx("span", {
1252
- className: "shrink-0 text-sm",
2201
+ className: "shrink-0 text-(length:--chat-ui-font-size) leading-5",
1253
2202
  children: hostOf(result.url)
1254
2203
  })
1255
2204
  ]
@@ -1257,18 +2206,44 @@ const webSearchTool = {
1257
2206
  });
1258
2207
  }
1259
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
+ };
1260
2234
  /** Memory lookups, e.g. Anthropic's memory tool. Input `{ path }`. */
1261
2235
  const memoryTool = {
1262
- label: () => "Recalled memory",
2236
+ label: (_, labels) => labels.recalledMemoryLabel,
1263
2237
  subtitle: (part) => {
1264
2238
  return stringOf(recordOf(part.input).path)?.split("/").at(-1) ?? part.subtitle;
1265
2239
  },
1266
- summary: () => "recalled memory"
2240
+ summary: (n, labels) => labels.recalledMemory(n)
1267
2241
  };
1268
2242
  const defaultToolViews = {
1269
2243
  bash: commandTool,
1270
2244
  shell: commandTool,
1271
2245
  web_search: webSearchTool,
2246
+ web_fetch: webFetchTool,
1272
2247
  memory: memoryTool,
1273
2248
  memory_view: memoryTool
1274
2249
  };
@@ -1280,9 +2255,8 @@ function hostOf(url) {
1280
2255
  }
1281
2256
  }
1282
2257
  /** "9s", "1m 5s". */
1283
- function formatDuration(ms) {
1284
- const seconds = Math.max(1, Math.round(ms / 1e3));
1285
- 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)));
1286
2260
  }
1287
2261
  // src/components/token-counter.tsx
1288
2262
  /** Compact token count: 526, 1.4k, 33k, 76.8k, 1M. */
@@ -1327,25 +2301,23 @@ function useCountUp(target) {
1327
2301
  }
1328
2302
  /** A token count that counts up as `tokens` grows and holds still when it stops growing. */
1329
2303
  function TokenCounter({ tokens, variant = "standalone", className }) {
2304
+ const icons = useIcons();
1330
2305
  const shown = useCountUp(tokens);
2306
+ const labels = useLabels();
1331
2307
  if (variant === "inline") return /* @__PURE__ */ jsxs("span", {
1332
2308
  className: clsx("text-chat-subtle tabular-nums", className),
1333
2309
  children: [
1334
2310
  "(",
1335
- formatTokens(shown),
1336
- " tokens)"
2311
+ labels.tokenCount(formatTokens(shown)),
2312
+ ")"
1337
2313
  ]
1338
2314
  });
1339
2315
  return /* @__PURE__ */ jsxs("span", {
1340
- className: clsx("inline-flex items-center gap-1 font-chat-sans text-xs text-chat-subtle tabular-nums", className),
1341
- children: [
1342
- /* @__PURE__ */ jsx(ArrowDown, {
1343
- "aria-hidden": true,
1344
- className: "size-3"
1345
- }),
1346
- formatTokens(shown),
1347
- " tokens"
1348
- ]
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))]
1349
2321
  });
1350
2322
  }
1351
2323
  // src/components/activity-group.tsx
@@ -1356,6 +2328,7 @@ function TokenCounter({ tokens, variant = "standalone", className }) {
1356
2328
  */
1357
2329
  function ActivityGroup({ parts, defaultExpanded = false, className }) {
1358
2330
  const views = useToolViews();
2331
+ const labels = useLabels();
1359
2332
  const [expanded, setExpanded] = useState(defaultExpanded);
1360
2333
  const live = parts.some(isWorking);
1361
2334
  const tokens = activityTokens(parts);
@@ -1377,9 +2350,9 @@ function ActivityGroup({ parts, defaultExpanded = false, className }) {
1377
2350
  onExpandedChange: setExpanded,
1378
2351
  summary: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Live, {
1379
2352
  live,
1380
- children: activityHeading(parts, views)
2353
+ children: activityHeading(parts, views, labels)
1381
2354
  }), after] }),
1382
- summaryClassName: "text-[0.9375rem]",
2355
+ summaryClassName: "text-(length:--chat-activity-font-size)",
1383
2356
  panelClassName: "pt-3",
1384
2357
  children: /* @__PURE__ */ jsx("div", {
1385
2358
  className: "divide-y divide-chat-divider rounded-chat-md border border-chat-divider",
@@ -1416,7 +2389,7 @@ function SingleStep({ part, live, after, expanded, onExpandedChange }) {
1416
2389
  live,
1417
2390
  children: summary
1418
2391
  }), after] }),
1419
- summaryClassName: "text-[0.9375rem]",
2392
+ summaryClassName: "text-(length:--chat-activity-font-size)",
1420
2393
  panelClassName: "pt-3",
1421
2394
  children: /* @__PURE__ */ jsx("div", {
1422
2395
  className: "rounded-chat-md border border-chat-divider px-3 py-2",
@@ -1429,7 +2402,7 @@ function Step({ part }) {
1429
2402
  const live = isWorking(part);
1430
2403
  return /* @__PURE__ */ jsx(Disclosure, {
1431
2404
  className: "px-3 py-2",
1432
- summaryClassName: "text-[0.9375rem]",
2405
+ summaryClassName: "text-(length:--chat-activity-font-size)",
1433
2406
  panelClassName: "pt-2 pb-1",
1434
2407
  summary: /* @__PURE__ */ jsx(Live, {
1435
2408
  live,
@@ -1440,12 +2413,13 @@ function Step({ part }) {
1440
2413
  }
1441
2414
  /** The label line and expanded body of one step. Tool calls go through their registered view. */
1442
2415
  function useStepContent(part) {
2416
+ const labels = useLabels();
1443
2417
  const view = useToolView(part.type === "tool_call" ? part.name : "");
1444
2418
  if (part.type === "thinking") return {
1445
- summary: activityLabel(part),
2419
+ summary: activityLabel(part, labels),
1446
2420
  detail: part.visibility === "hidden" || !part.text ? /* @__PURE__ */ jsx("p", {
1447
- className: "text-sm text-chat-subtle",
1448
- 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
1449
2423
  }) : /* @__PURE__ */ jsx("div", {
1450
2424
  className: "max-h-96 overflow-y-auto [scrollbar-width:thin]",
1451
2425
  children: /* @__PURE__ */ jsx(Markdown, {
@@ -1460,7 +2434,7 @@ function useStepContent(part) {
1460
2434
  return {
1461
2435
  summary: /* @__PURE__ */ jsxs("span", {
1462
2436
  title: view.hint?.(part),
1463
- children: [activityLabel(part, view), subtitle && /* @__PURE__ */ jsx("span", {
2437
+ children: [activityLabel(part, labels, view), subtitle && /* @__PURE__ */ jsx("span", {
1464
2438
  className: "ml-1.5 text-chat-fg",
1465
2439
  children: subtitle
1466
2440
  })]
@@ -1470,16 +2444,16 @@ function useStepContent(part) {
1470
2444
  }
1471
2445
  function DefaultDetail({ part }) {
1472
2446
  if (part.error) return /* @__PURE__ */ jsx("p", {
1473
- className: "text-sm text-chat-danger",
2447
+ className: "text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
1474
2448
  children: part.error
1475
2449
  });
1476
2450
  const body = part.result ?? part.input ?? part.inputJson;
1477
2451
  if (typeof body === "string") return /* @__PURE__ */ jsx("p", {
1478
- 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",
1479
2453
  children: body
1480
2454
  });
1481
2455
  return /* @__PURE__ */ jsx("pre", {
1482
- 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",
1483
2457
  children: JSON.stringify(body, null, 2)
1484
2458
  });
1485
2459
  }
@@ -1490,6 +2464,9 @@ const draftId = () => `att_${Date.now().toString(36)}_${(counter++).toString(36)
1490
2464
  function useAttachments({ upload } = {}) {
1491
2465
  const [attachments, setAttachments] = useState([]);
1492
2466
  const uploads = useRef(/* @__PURE__ */ new Map());
2467
+ const labels = useLabels();
2468
+ const labelsRef = useRef(labels);
2469
+ labelsRef.current = labels;
1493
2470
  const uploadRef = useRef(upload);
1494
2471
  uploadRef.current = upload;
1495
2472
  const patch = useCallback((id, fields) => {
@@ -1523,7 +2500,7 @@ function useAttachments({ upload } = {}) {
1523
2500
  })).catch((error) => {
1524
2501
  if (!controller.signal.aborted) patch(id, {
1525
2502
  status: "error",
1526
- error: error instanceof Error ? error.message : "Upload failed"
2503
+ error: error instanceof Error ? error.message : labelsRef.current.uploadFailed
1527
2504
  });
1528
2505
  }).finally(() => uploads.current.delete(id));
1529
2506
  }
@@ -1532,7 +2509,7 @@ function useAttachments({ upload } = {}) {
1532
2509
  setAttachments((list) => [...list, {
1533
2510
  id: draftId(),
1534
2511
  kind: "text",
1535
- name: "Pasted text",
2512
+ name: labelsRef.current.pastedText,
1536
2513
  mime: "text/plain",
1537
2514
  size: new Blob([text]).size,
1538
2515
  text,
@@ -1578,13 +2555,15 @@ function useAttachments({ upload } = {}) {
1578
2555
  const extension = (name) => /\.([a-z0-9]{1,5})$/i.exec(name)?.[1]?.toUpperCase();
1579
2556
  /** A square card: pasted text preview with a PASTED badge, an image thumbnail, or a file with its type. */
1580
2557
  function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", className }) {
2558
+ const icons = useIcons();
1581
2559
  const image = a.mime.startsWith("image/") && a.url;
1582
2560
  const pasted = a.text !== void 0;
2561
+ const labels = useLabels();
1583
2562
  const uploading = a.status === "uploading";
1584
2563
  const failed = a.status === "error";
1585
2564
  return /* @__PURE__ */ jsxs("div", {
1586
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),
1587
- title: failed ? a.error ?? "Upload failed" : a.name,
2566
+ title: failed ? a.error ?? labels.uploadFailed : a.name,
1588
2567
  children: [
1589
2568
  /* @__PURE__ */ jsx("div", {
1590
2569
  className: clsx("absolute inset-0 overflow-hidden rounded-[inherit]", uploading && "chat-shimmer-surface"),
@@ -1597,11 +2576,11 @@ function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", classNam
1597
2576
  children: a.text
1598
2577
  }) : /* @__PURE__ */ jsxs("div", {
1599
2578
  className: "flex h-full flex-col gap-1.5 p-2.5",
1600
- children: [/* @__PURE__ */ jsx(FileText, {
1601
- "aria-hidden": true,
1602
- 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
1603
2582
  }), /* @__PURE__ */ jsx("span", {
1604
- 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",
1605
2584
  children: a.name
1606
2585
  })]
1607
2586
  })
@@ -1610,29 +2589,29 @@ function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", classNam
1610
2589
  className: "absolute inset-x-2 bottom-2 flex items-end justify-between gap-1",
1611
2590
  children: [/* @__PURE__ */ jsx("span", {
1612
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"),
1613
- children: failed ? "Failed" : pasted ? "Pasted" : extension(a.name) ?? "File"
2592
+ children: failed ? labels.failed : pasted ? labels.pasted : extension(a.name) ?? labels.file
1614
2593
  }), pasted && onOpen && /* @__PURE__ */ jsx(IconButton, {
1615
- label: "Edit pasted text",
2594
+ label: labels.editPastedText,
1616
2595
  size: "sm",
1617
2596
  onPress: onOpen,
1618
2597
  className: "size-6 border border-chat-border-strong bg-chat-bg [&_svg]:size-3.5 [@media(pointer:coarse)]:size-7!",
1619
- children: /* @__PURE__ */ jsx(Pencil, {})
2598
+ children: icons.edit
1620
2599
  })]
1621
2600
  }),
1622
2601
  pasted && onOpen && !onRemove && /* @__PURE__ */ jsx("button", {
1623
2602
  type: "button",
1624
- "aria-label": "View pasted text",
2603
+ "aria-label": labels.viewPastedText,
1625
2604
  onClick: onOpen,
1626
2605
  className: "absolute inset-0 cursor-pointer rounded-[inherit] outline-none focus-visible:ring-2 focus-visible:ring-chat-focus"
1627
2606
  }),
1628
2607
  onRemove && /* @__PURE__ */ jsx(IconButton, {
1629
- label: `Remove ${a.name}`,
2608
+ label: labels.removeAttachment(a.name),
1630
2609
  variant: "subtle",
1631
2610
  size: "sm",
1632
2611
  tooltip: false,
1633
2612
  onPress: onRemove,
1634
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"),
1635
- children: /* @__PURE__ */ jsx(X, {})
2614
+ children: icons.remove
1636
2615
  })
1637
2616
  ]
1638
2617
  });
@@ -1640,9 +2619,10 @@ function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", classNam
1640
2619
  /** The composer's row of cards above the text. Renders nothing when empty. */
1641
2620
  function AttachmentRow({ attachments, onRemove, onEditText }) {
1642
2621
  const [editing, setEditing] = useState();
2622
+ const labels = useLabels();
1643
2623
  if (attachments.length === 0) return null;
1644
2624
  return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("ul", {
1645
- "aria-label": "Attachments",
2625
+ "aria-label": labels.attachments,
1646
2626
  className: "flex gap-2.5 overflow-x-auto px-0.5 pt-2.5 pb-1 [scrollbar-width:thin]",
1647
2627
  children: attachments.map((a) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(AttachmentCard, {
1648
2628
  attachment: {
@@ -1659,7 +2639,9 @@ function AttachmentRow({ attachments, onRemove, onEditText }) {
1659
2639
  })] });
1660
2640
  }
1661
2641
  /** Shows pasted text in a modal, editable when `onSave` is set. */
1662
- function PastedTextDialog({ text, onClose, onSave, title = "Pasted text" }) {
2642
+ function PastedTextDialog({ text, onClose, onSave, title }) {
2643
+ const labels = useLabels();
2644
+ title ??= labels.pastedText;
1663
2645
  const [draft, setDraft] = useState(text ?? "");
1664
2646
  useEffect(() => setDraft(text ?? ""), [text]);
1665
2647
  return /* @__PURE__ */ jsxs(Modal, {
@@ -1671,34 +2653,34 @@ function PastedTextDialog({ text, onClose, onSave, title = "Pasted text" }) {
1671
2653
  onChange: (e) => setDraft(e.target.value),
1672
2654
  readOnly: !onSave,
1673
2655
  "aria-label": title,
1674
- 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]"
1675
2657
  }), /* @__PURE__ */ jsxs("div", {
1676
2658
  className: "flex justify-end gap-2 px-5 py-4",
1677
2659
  children: [/* @__PURE__ */ jsx(Button, {
1678
2660
  variant: "subtle",
1679
2661
  onPress: onClose,
1680
- children: onSave ? "Cancel" : "Close"
2662
+ children: onSave ? labels.cancel : labels.close
1681
2663
  }), onSave && /* @__PURE__ */ jsx(Button, {
1682
2664
  variant: "primary",
1683
2665
  onPress: () => {
1684
2666
  onSave(draft);
1685
2667
  onClose();
1686
2668
  },
1687
- children: "Save"
2669
+ children: labels.save
1688
2670
  })]
1689
2671
  })]
1690
2672
  });
1691
2673
  }
1692
2674
  // src/components/messages.tsx
1693
2675
  /** "just now", "5 minutes ago", "21 hours ago", "yesterday", then a date. */
1694
- function formatRelativeTime(iso, now = Date.now()) {
2676
+ function formatRelativeTime(iso, now = Date.now(), labels = defaultLabels) {
1695
2677
  const seconds = (new Date(iso).getTime() - now) / 1e3;
1696
- const rtf = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
1697
- 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;
1698
2680
  if (Math.abs(seconds) < 3600) return rtf.format(Math.round(seconds / 60), "minute");
1699
2681
  if (Math.abs(seconds) < 86400) return rtf.format(Math.round(seconds / 3600), "hour");
1700
2682
  if (Math.abs(seconds) < 604800) return rtf.format(Math.round(seconds / 86400), "day");
1701
- return new Date(iso).toLocaleDateString(void 0, {
2683
+ return new Date(iso).toLocaleDateString(labels.locale, {
1702
2684
  month: "short",
1703
2685
  day: "numeric"
1704
2686
  });
@@ -1708,21 +2690,25 @@ function messageText(message) {
1708
2690
  return message.parts.map((p) => p.type === "text" ? p.text : p.type === "custom" ? p.text ?? "" : "").filter(Boolean).join("\n\n");
1709
2691
  }
1710
2692
  function CopyButton({ text }) {
2693
+ const icons = useIcons();
2694
+ const labels = useLabels();
1711
2695
  const [copied, setCopied] = useState(false);
1712
2696
  return /* @__PURE__ */ jsx(IconButton, {
1713
- label: copied ? "Copied" : "Copy",
2697
+ label: copied ? labels.copied : labels.copy,
1714
2698
  size: "sm",
1715
2699
  onPress: () => {
1716
2700
  navigator.clipboard?.writeText(text);
1717
2701
  setCopied(true);
1718
2702
  setTimeout(() => setCopied(false), 1500);
1719
2703
  },
1720
- children: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {})
2704
+ children: copied ? icons.copied : icons.copy
1721
2705
  });
1722
2706
  }
1723
- 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";
1724
2708
  /** The user's turn: a bubble on the right, with time, retry, edit and copy under it on hover. */
1725
2709
  function UserMessage({ message, onRetry, onEdit, actions, className }) {
2710
+ const icons = useIcons();
2711
+ const labels = useLabels();
1726
2712
  const text = messageText(message);
1727
2713
  const attachments = message.parts.filter((p) => p.type === "attachment");
1728
2714
  const [viewing, setViewing] = useState();
@@ -1730,7 +2716,7 @@ function UserMessage({ message, onRetry, onEdit, actions, className }) {
1730
2716
  className: clsx("group/message flex flex-col items-end", className),
1731
2717
  children: [
1732
2718
  attachments.length > 0 && /* @__PURE__ */ jsx("ul", {
1733
- "aria-label": "Attachments",
2719
+ "aria-label": labels.attachments,
1734
2720
  className: "mb-2 flex max-w-[85%] flex-wrap justify-end gap-2",
1735
2721
  children: attachments.map((a) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(AttachmentCard, {
1736
2722
  attachment: a,
@@ -1742,28 +2728,25 @@ function UserMessage({ message, onRetry, onEdit, actions, className }) {
1742
2728
  text: viewing,
1743
2729
  onClose: () => setViewing(void 0)
1744
2730
  }),
1745
- text && /* @__PURE__ */ jsx("div", {
1746
- 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",
1747
- children: text
1748
- }),
2731
+ text && /* @__PURE__ */ jsx(UserBubble, { text }),
1749
2732
  /* @__PURE__ */ jsxs("div", {
1750
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",
1751
2734
  children: [
1752
2735
  /* @__PURE__ */ jsx("span", {
1753
2736
  className: timeClass,
1754
- children: formatRelativeTime(message.createdAt)
2737
+ children: formatRelativeTime(message.createdAt, void 0, labels)
1755
2738
  }),
1756
2739
  onRetry && /* @__PURE__ */ jsx(IconButton, {
1757
- label: "Retry",
2740
+ label: labels.retry,
1758
2741
  size: "sm",
1759
2742
  onPress: onRetry,
1760
- children: /* @__PURE__ */ jsx(RefreshCw, {})
2743
+ children: icons.retry
1761
2744
  }),
1762
2745
  onEdit && /* @__PURE__ */ jsx(IconButton, {
1763
- label: "Edit",
2746
+ label: labels.edit,
1764
2747
  size: "sm",
1765
2748
  onPress: onEdit,
1766
- children: /* @__PURE__ */ jsx(Pencil, {})
2749
+ children: icons.edit
1767
2750
  }),
1768
2751
  /* @__PURE__ */ jsx(CopyButton, { text }),
1769
2752
  actions
@@ -1772,11 +2755,46 @@ function UserMessage({ message, onRetry, onEdit, actions, className }) {
1772
2755
  ]
1773
2756
  });
1774
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
+ }
1775
2791
  /**
1776
2792
  * The assistant's turn: activity groups (each with its own token count), markdown, and other parts in
1777
2793
  * order. When done, copy / read aloud / feedback / retry and the time sit under it.
1778
2794
  */
1779
2795
  function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPart, actions, className }) {
2796
+ const icons = useIcons();
2797
+ const labels = useLabels();
1780
2798
  const streaming = message.status === "streaming";
1781
2799
  const views = useToolViews();
1782
2800
  const blocks = toBlocks(message.parts, (part) => views[part.name]?.Block !== void 0);
@@ -1784,56 +2802,66 @@ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPar
1784
2802
  className: clsx("group/message flex flex-col gap-4", className),
1785
2803
  children: [
1786
2804
  blocks.map((block) => {
1787
- 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
+ }
1788
2814
  if (block.kind === "tool") {
1789
2815
  const ToolBlock = views[block.part.name].Block;
1790
2816
  return /* @__PURE__ */ jsx(ToolBlock, { part: block.part }, block.id);
1791
2817
  }
1792
2818
  const part = block.part;
1793
2819
  if (part.type === "text") return /* @__PURE__ */ jsx(Markdown, {
2820
+ partId: part.id,
1794
2821
  streaming: part.status === "streaming",
1795
2822
  children: part.text
1796
2823
  }, part.id);
1797
2824
  const custom = renderPart?.(part);
1798
2825
  if (custom) return /* @__PURE__ */ jsx("div", { children: custom }, part.id);
2826
+ if (part.type === "widget") return /* @__PURE__ */ jsx(WidgetPartView, { part }, part.id);
1799
2827
  if (part.type === "custom" && part.text) return /* @__PURE__ */ jsx(Markdown, { children: part.text }, part.id);
1800
2828
  return null;
1801
2829
  }),
1802
2830
  message.status === "error" && /* @__PURE__ */ jsx("p", {
1803
- className: "font-chat-sans text-sm text-chat-danger",
1804
- 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
1805
2833
  }),
1806
2834
  streaming ? /* @__PURE__ */ jsx("div", { className: "h-7" }) : /* @__PURE__ */ jsxs("div", {
1807
2835
  className: "-ml-1.5 flex h-7 items-center gap-0.5",
1808
2836
  children: [
1809
2837
  /* @__PURE__ */ jsx(CopyButton, { text: messageText(message) }),
1810
2838
  onReadAloud && /* @__PURE__ */ jsx(IconButton, {
1811
- label: "Read aloud",
2839
+ label: labels.readAloud,
1812
2840
  size: "sm",
1813
2841
  onPress: onReadAloud,
1814
- children: /* @__PURE__ */ jsx(Volume2, {})
2842
+ children: icons.speak
1815
2843
  }),
1816
2844
  onFeedback && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(IconButton, {
1817
- label: "Good response",
2845
+ label: labels.goodResponse,
1818
2846
  size: "sm",
1819
2847
  onPress: () => onFeedback("good"),
1820
- children: /* @__PURE__ */ jsx(ThumbsUp, {})
2848
+ children: icons.thumbsUp
1821
2849
  }), /* @__PURE__ */ jsx(IconButton, {
1822
- label: "Bad response",
2850
+ label: labels.badResponse,
1823
2851
  size: "sm",
1824
2852
  onPress: () => onFeedback("bad"),
1825
- children: /* @__PURE__ */ jsx(ThumbsDown, {})
2853
+ children: icons.thumbsDown
1826
2854
  })] }),
1827
2855
  onRetry && /* @__PURE__ */ jsx(IconButton, {
1828
- label: "Retry",
2856
+ label: labels.retry,
1829
2857
  size: "sm",
1830
2858
  onPress: onRetry,
1831
- children: /* @__PURE__ */ jsx(RefreshCw, {})
2859
+ children: icons.retry
1832
2860
  }),
1833
2861
  actions,
1834
2862
  /* @__PURE__ */ jsx("span", {
1835
2863
  className: timeClass,
1836
- children: formatRelativeTime(message.createdAt)
2864
+ children: formatRelativeTime(message.createdAt, void 0, labels)
1837
2865
  })
1838
2866
  ]
1839
2867
  })
@@ -1846,7 +2874,9 @@ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPar
1846
2874
  * view's `Detail` when it has one, else the input. Replying in the composer instead declines it.
1847
2875
  */
1848
2876
  function ApprovalCard({ approval, onRespond, className }) {
2877
+ const icons = useIcons();
1849
2878
  const { part } = approval;
2879
+ const labels = useLabels();
1850
2880
  const view = useToolView(part.name);
1851
2881
  const Detail = view.Detail;
1852
2882
  const preview = {
@@ -1855,21 +2885,17 @@ function ApprovalCard({ approval, onRespond, className }) {
1855
2885
  };
1856
2886
  return /* @__PURE__ */ jsxs("div", {
1857
2887
  role: "alertdialog",
1858
- "aria-label": `Allow ${part.name}?`,
2888
+ "aria-label": labels.allowTool(part.name),
1859
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),
1860
2890
  children: [
1861
2891
  /* @__PURE__ */ jsxs("div", {
1862
- className: "flex items-center gap-2 text-sm",
1863
- children: [/* @__PURE__ */ jsx(ShieldQuestion, {
1864
- "aria-hidden": true,
1865
- 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
1866
2896
  }), /* @__PURE__ */ jsx("span", {
1867
2897
  className: "min-w-0 flex-1 font-medium",
1868
- children: view.ask?.(part) ?? /* @__PURE__ */ jsxs(Fragment, { children: [
1869
- "Allow ",
1870
- activityLabel(part, view),
1871
- "?"
1872
- ] })
2898
+ children: view.ask?.(part) ?? labels.allowAction(activityLabel(part, labels, view))
1873
2899
  })]
1874
2900
  }),
1875
2901
  /* @__PURE__ */ jsx("div", {
@@ -1886,12 +2912,12 @@ function ApprovalCard({ approval, onRespond, className }) {
1886
2912
  variant: "ghost",
1887
2913
  className: "border border-chat-border-strong",
1888
2914
  onPress: () => onRespond(part.id, false),
1889
- children: "Deny"
2915
+ children: labels.deny
1890
2916
  }), /* @__PURE__ */ jsx(Button, {
1891
2917
  variant: "primary",
1892
2918
  autoFocus: true,
1893
2919
  onPress: () => onRespond(part.id, true),
1894
- children: "Allow"
2920
+ children: labels.allow
1895
2921
  })]
1896
2922
  })
1897
2923
  ]
@@ -1904,7 +2930,8 @@ const DEFAULT_COLOR = "var(--chat-subtle)";
1904
2930
  * What fills the context window: a usage line, a segmented bar, one row per category with its share, and
1905
2931
  * expandable lists (each MCP tool, each memory file). Deferred categories are listed but not counted.
1906
2932
  */
1907
- function ContextPanel({ usage, title = "Context", defaultExpanded = true, className }) {
2933
+ function ContextPanel({ usage, title, defaultExpanded = true, className }) {
2934
+ const labels = useLabels();
1908
2935
  const counted = usage.categories.filter((c) => !c.deferred);
1909
2936
  const deferred = usage.categories.filter((c) => c.deferred);
1910
2937
  const used = counted.reduce((sum, c) => sum + (c.reserved ? 0 : c.tokens), 0);
@@ -1912,16 +2939,16 @@ function ContextPanel({ usage, title = "Context", defaultExpanded = true, classN
1912
2939
  const free = Math.max(usage.limit - used - reserved, 0);
1913
2940
  const share = (tokens) => formatShare(tokens / usage.limit);
1914
2941
  return /* @__PURE__ */ jsxs(Disclosure, {
1915
- summary: title,
2942
+ summary: title ?? labels.context,
1916
2943
  defaultExpanded,
1917
2944
  className: clsx("font-chat-sans text-chat-fg", className),
1918
- summaryClassName: "text-[0.9375rem] text-chat-fg",
2945
+ summaryClassName: "text-(length:--chat-activity-font-size) text-chat-fg",
1919
2946
  panelClassName: "pt-3",
1920
2947
  children: [
1921
2948
  /* @__PURE__ */ jsxs("div", {
1922
- className: "mb-2 flex items-baseline justify-between text-[0.9375rem]",
1923
- children: [/* @__PURE__ */ jsxs("span", { children: [Math.round(used / usage.limit * 100), "% used"] }), /* @__PURE__ */ jsxs("span", {
1924
- 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",
1925
2952
  children: [
1926
2953
  formatTokens(used),
1927
2954
  " / ",
@@ -1931,7 +2958,7 @@ function ContextPanel({ usage, title = "Context", defaultExpanded = true, classN
1931
2958
  }),
1932
2959
  /* @__PURE__ */ jsx("div", {
1933
2960
  role: "meter",
1934
- "aria-label": "Context used",
2961
+ "aria-label": labels.contextUsed,
1935
2962
  "aria-valuemin": 0,
1936
2963
  "aria-valuemax": usage.limit,
1937
2964
  "aria-valuenow": used,
@@ -1953,7 +2980,7 @@ function ContextPanel({ usage, title = "Context", defaultExpanded = true, classN
1953
2980
  }, c.id)), /* @__PURE__ */ jsx(CategoryRow, {
1954
2981
  category: {
1955
2982
  id: "free",
1956
- label: "Free space",
2983
+ label: labels.freeSpace,
1957
2984
  tokens: free,
1958
2985
  color: FREE_COLOR
1959
2986
  },
@@ -1984,7 +3011,7 @@ function CategoryRow({ category, share }) {
1984
3011
  }) })
1985
3012
  }) : category.label;
1986
3013
  return /* @__PURE__ */ jsxs("li", {
1987
- 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",
1988
3015
  children: [
1989
3016
  /* @__PURE__ */ jsx("span", {
1990
3017
  "aria-hidden": true,
@@ -2010,16 +3037,16 @@ function GroupList({ group }) {
2010
3037
  return /* @__PURE__ */ jsx(Disclosure, {
2011
3038
  summary: group.label,
2012
3039
  trailing: /* @__PURE__ */ jsx("span", {
2013
- className: "text-sm text-chat-subtle tabular-nums",
3040
+ className: "text-(length:--chat-ui-font-size) leading-5 text-chat-subtle tabular-nums",
2014
3041
  children: group.items.length
2015
3042
  }),
2016
3043
  indicator: "chevron",
2017
- summaryClassName: "text-sm",
3044
+ summaryClassName: "text-(length:--chat-ui-font-size) leading-5",
2018
3045
  panelClassName: "pt-1.5",
2019
3046
  children: /* @__PURE__ */ jsx("ul", {
2020
3047
  className: "max-h-72 space-y-1 overflow-y-auto pl-6 [scrollbar-color:var(--chat-border-strong)_transparent] [scrollbar-width:thin]",
2021
3048
  children: group.items.map((item) => /* @__PURE__ */ jsxs("li", {
2022
- 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",
2023
3050
  children: [/* @__PURE__ */ jsx("span", {
2024
3051
  className: "min-w-0 flex-1 truncate",
2025
3052
  children: item.label
@@ -2038,7 +3065,9 @@ function formatShare(fraction) {
2038
3065
  /** Touch keyboards have no easy Shift+Enter, so there Enter adds a line and the button sends. */
2039
3066
  const coarsePointer = () => typeof window !== "undefined" && window.matchMedia?.("(pointer: coarse)").matches;
2040
3067
  /** The chat box: growing textarea, toolbar slots, send / stop. Enter sends, Shift+Enter adds a line. */
2041
- 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();
2042
3071
  const [own, setOwn] = useState("");
2043
3072
  const text = value ?? own;
2044
3073
  const setText = (next) => {
@@ -2111,18 +3140,18 @@ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
2111
3140
  };
2112
3141
  const compact = variant === "compact";
2113
3142
  const action = busy ? /* @__PURE__ */ jsx(IconButton, {
2114
- label: "Stop",
3143
+ label: labels.stop,
2115
3144
  variant: "primary",
2116
3145
  onPress: onStop,
2117
3146
  className: "rounded-chat-md [&_svg]:size-3.5 [&_svg]:fill-current",
2118
- children: /* @__PURE__ */ jsx(Square, {})
3147
+ children: icons.stop
2119
3148
  }) : /* @__PURE__ */ jsx(IconButton, {
2120
- label: "Send",
3149
+ label: labels.send,
2121
3150
  variant: "primary",
2122
3151
  onPress: submit,
2123
3152
  isDisabled: !canSend,
2124
3153
  className: "rounded-chat-md",
2125
- children: /* @__PURE__ */ jsx(ArrowUp, {})
3154
+ children: icons.send
2126
3155
  });
2127
3156
  return /* @__PURE__ */ jsxs("div", {
2128
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),
@@ -2144,8 +3173,8 @@ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
2144
3173
  onChange: (e) => setText(e.target.value),
2145
3174
  onKeyDown,
2146
3175
  onPaste,
2147
- placeholder,
2148
- "aria-label": "Message",
3176
+ placeholder: placeholder ?? labels.replyPlaceholder,
3177
+ "aria-label": labels.message,
2149
3178
  disabled: isDisabled,
2150
3179
  autoFocus,
2151
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")
@@ -2169,10 +3198,12 @@ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
2169
3198
  });
2170
3199
  }
2171
3200
  /** The `+` button that opens the attach menu above the composer. */
2172
- function AttachButton({ children, label = "Add files and more" }) {
3201
+ function AttachButton({ children, label }) {
3202
+ const icons = useIcons();
3203
+ const labels = useLabels();
2173
3204
  return /* @__PURE__ */ jsxs(MenuTrigger, { children: [/* @__PURE__ */ jsx(IconButton, {
2174
- label,
2175
- children: /* @__PURE__ */ jsx(Plus, {})
3205
+ label: label ?? labels.addFiles,
3206
+ children: icons.attach
2176
3207
  }), /* @__PURE__ */ jsx(Popover, {
2177
3208
  placement: "top start",
2178
3209
  children
@@ -2180,7 +3211,9 @@ function AttachButton({ children, label = "Add files and more" }) {
2180
3211
  }
2181
3212
  // src/components/model-picker.tsx
2182
3213
  /** "Opus 5.5 High ⌄": the models, an Effort submenu for models that have levels, and More models. */
2183
- 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();
2184
3217
  const current = models.find((m) => m.id === model);
2185
3218
  const efforts = current?.efforts ?? [];
2186
3219
  const currentEffort = efforts.find((e) => e.id === (effort ?? current?.defaultEffort));
@@ -2199,7 +3232,7 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
2199
3232
  };
2200
3233
  return /* @__PURE__ */ jsxs(MenuTrigger, { children: [/* @__PURE__ */ jsxs(Button, {
2201
3234
  size: "sm",
2202
- "aria-label": `Model: ${current?.label ?? model}`,
3235
+ "aria-label": labels.modelButton(current?.label ?? model),
2203
3236
  className: clsx("text-chat-muted data-hovered:text-chat-fg", className),
2204
3237
  children: [
2205
3238
  /* @__PURE__ */ jsx("span", {
@@ -2210,16 +3243,16 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
2210
3243
  className: "chat-picker-detail text-chat-muted",
2211
3244
  children: currentEffort.label
2212
3245
  }),
2213
- /* @__PURE__ */ jsx(ChevronDown, {
2214
- "aria-hidden": true,
2215
- className: "size-3.5"
3246
+ /* @__PURE__ */ jsx(IconSlot, {
3247
+ className: "[&_svg]:size-3.5",
3248
+ children: icons.dropdown
2216
3249
  })
2217
3250
  ]
2218
3251
  }), /* @__PURE__ */ jsxs(Popover, {
2219
3252
  placement: "top end",
2220
3253
  className: "w-72",
2221
3254
  children: [/* @__PURE__ */ jsxs(Menu, {
2222
- "aria-label": "Model",
3255
+ "aria-label": labels.model,
2223
3256
  children: [
2224
3257
  /* @__PURE__ */ jsx(MenuSection, {
2225
3258
  selectionMode: "single",
@@ -2231,12 +3264,12 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
2231
3264
  efforts.length > 0 && onEffortChange && /* @__PURE__ */ jsxs(SubmenuTrigger, { children: [/* @__PURE__ */ jsx(MenuItem, {
2232
3265
  id: "__effort",
2233
3266
  detail: currentEffort?.label,
2234
- children: "Effort"
3267
+ children: labels.effort
2235
3268
  }), /* @__PURE__ */ jsxs(Popover, {
2236
3269
  placement: "end top",
2237
3270
  className: "w-80",
2238
- children: [/* @__PURE__ */ jsx(MenuNote, { children: effortNote }), /* @__PURE__ */ jsx(Menu, {
2239
- "aria-label": "Effort",
3271
+ children: [/* @__PURE__ */ jsx(MenuNote, { children: effortNote ?? labels.effortNote }), /* @__PURE__ */ jsx(Menu, {
3272
+ "aria-label": labels.effort,
2240
3273
  selectionMode: "single",
2241
3274
  selectedKeys: currentEffort ? [currentEffort.id] : [],
2242
3275
  onSelectionChange: (keys) => {
@@ -2251,10 +3284,10 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
2251
3284
  className: "flex items-center gap-2",
2252
3285
  children: [
2253
3286
  e.label,
2254
- e.recommended && /* @__PURE__ */ jsx(Badge, { children: "Recommended" }),
3287
+ e.recommended && /* @__PURE__ */ jsx(Badge, { children: labels.recommended }),
2255
3288
  e.warning && /* @__PURE__ */ jsx(Badge, {
2256
3289
  tone: "warning",
2257
- icon: /* @__PURE__ */ jsx(AlertTriangle, {}),
3290
+ icon: icons.warning,
2258
3291
  children: e.warning
2259
3292
  })
2260
3293
  ]
@@ -2264,12 +3297,12 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
2264
3297
  })] }),
2265
3298
  more.length > 0 && /* @__PURE__ */ jsxs(SubmenuTrigger, { children: [/* @__PURE__ */ jsx(MenuItem, {
2266
3299
  id: "__more",
2267
- children: "More models"
3300
+ children: labels.moreModels
2268
3301
  }), /* @__PURE__ */ jsx(Popover, {
2269
3302
  placement: "end top",
2270
3303
  className: "w-72",
2271
3304
  children: /* @__PURE__ */ jsx(Menu, {
2272
- "aria-label": "More models",
3305
+ "aria-label": labels.moreModels,
2273
3306
  selectionMode: "single",
2274
3307
  selectedKeys: [model],
2275
3308
  onSelectionChange: pick,
@@ -2278,7 +3311,7 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
2278
3311
  })] })
2279
3312
  ]
2280
3313
  }), footnote && /* @__PURE__ */ jsx(MenuNote, {
2281
- 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",
2282
3315
  children: footnote
2283
3316
  })]
2284
3317
  })] });
@@ -2290,10 +3323,12 @@ const STICK_SLOP = 48;
2290
3323
  * The scrolling conversation. It follows a streaming reply while the reader is at the bottom and leaves
2291
3324
  * them alone once they scroll up, with a button to jump back. Sending a message always jumps down.
2292
3325
  */
2293
- 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();
2294
3328
  const scroller = useRef(null);
2295
3329
  const content = useRef(null);
2296
- const stuck = useRef(true);
3330
+ const stuck = useRef(start === "bottom");
3331
+ const labels = useLabels();
2297
3332
  const [atBottom, setAtBottom] = useState(true);
2298
3333
  const toBottom = useCallback((behavior = "auto") => {
2299
3334
  const el = scroller.current;
@@ -2319,7 +3354,13 @@ function MessageList({ messages, onRetry, onEdit, onFeedback, renderPart, footer
2319
3354
  return () => observer.disconnect();
2320
3355
  }, [toBottom]);
2321
3356
  const lastUser = messages.findLast((m) => m.role === "user")?.id;
3357
+ const jumpedFor = useRef(start === "top" ? lastUser : null);
2322
3358
  useLayoutEffect(() => {
3359
+ if (lastUser === jumpedFor.current) {
3360
+ onScroll();
3361
+ return;
3362
+ }
3363
+ jumpedFor.current = lastUser;
2323
3364
  stuck.current = true;
2324
3365
  setAtBottom(true);
2325
3366
  toBottom();
@@ -2333,11 +3374,11 @@ function MessageList({ messages, onRetry, onEdit, onFeedback, renderPart, footer
2333
3374
  children: /* @__PURE__ */ jsxs("div", {
2334
3375
  ref: content,
2335
3376
  className: "flex min-h-full flex-col",
2336
- children: [/* @__PURE__ */ jsx("div", {
3377
+ children: [/* @__PURE__ */ jsxs("div", {
2337
3378
  role: "log",
2338
- "aria-label": "Conversation",
3379
+ "aria-label": labels.conversation,
2339
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",
2340
- children: messages.map((m) => m.role === "user" ? /* @__PURE__ */ jsx(UserMessage, {
3381
+ children: [header, messages.map((m) => m.role === "user" ? /* @__PURE__ */ jsx(UserMessage, {
2341
3382
  message: m,
2342
3383
  ...onEdit && { onEdit: () => onEdit(m.id) }
2343
3384
  }, m.id) : /* @__PURE__ */ jsx(AssistantMessage, {
@@ -2345,27 +3386,28 @@ function MessageList({ messages, onRetry, onEdit, onFeedback, renderPart, footer
2345
3386
  ...onRetry && { onRetry: () => onRetry(m.id) },
2346
3387
  ...onFeedback && { onFeedback: (f) => onFeedback(m.id, f) },
2347
3388
  ...renderPart && { renderPart }
2348
- }, m.id))
3389
+ }, m.id))]
2349
3390
  }), footer && /* @__PURE__ */ jsx("div", {
2350
3391
  className: "sticky bottom-0",
2351
3392
  children: footer
2352
3393
  })]
2353
3394
  })
2354
3395
  }), !atBottom && /* @__PURE__ */ jsx(IconButton, {
2355
- label: "Scroll to bottom",
3396
+ label: labels.scrollToBottom,
2356
3397
  variant: "subtle",
2357
3398
  onPress: () => {
2358
3399
  stuck.current = true;
2359
3400
  toBottom("smooth");
2360
3401
  },
2361
3402
  className: "absolute bottom-[min(10rem,40%)] left-1/2 -translate-x-1/2 rounded-full border border-chat-border shadow-chat-popover",
2362
- children: /* @__PURE__ */ jsx(ArrowDown, {})
3403
+ children: icons.scrollToBottom
2363
3404
  })]
2364
3405
  });
2365
3406
  }
2366
3407
  // src/components/chat-header.tsx
2367
3408
  /** The bar above a chat: breadcrumb and title with its menu, actions on the right. */
2368
3409
  function ChatHeader({ title, menu, leading, actions, className }) {
3410
+ const icons = useIcons();
2369
3411
  return /* @__PURE__ */ jsxs("header", {
2370
3412
  className: clsx("flex h-12 shrink-0 items-center gap-1 px-3 font-chat-sans", className),
2371
3413
  children: [
@@ -2376,15 +3418,15 @@ function ChatHeader({ title, menu, leading, actions, className }) {
2376
3418
  children: [/* @__PURE__ */ jsx("span", {
2377
3419
  className: "truncate",
2378
3420
  children: title
2379
- }), /* @__PURE__ */ jsx(ChevronDown, {
2380
- "aria-hidden": true,
2381
- 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
2382
3424
  })]
2383
3425
  }), /* @__PURE__ */ jsx(Popover, {
2384
3426
  placement: "bottom start",
2385
3427
  children: menu
2386
3428
  })] }) : /* @__PURE__ */ jsx("h1", {
2387
- 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",
2388
3430
  children: title
2389
3431
  }),
2390
3432
  /* @__PURE__ */ jsx("div", {
@@ -2396,6 +3438,8 @@ function ChatHeader({ title, menu, leading, actions, className }) {
2396
3438
  }
2397
3439
  /** Pin, Rename, Add to project (searchable), Share, Delete. Each shows only when its handler is set. */
2398
3440
  function ChatTitleMenu({ pinned, onPin, onRename, projects, onAddToProject, onCreateProject, onShare, onDelete, children }) {
3441
+ const icons = useIcons();
3442
+ const labels = useLabels();
2399
3443
  const act = (key) => {
2400
3444
  if (key === "pin") onPin?.();
2401
3445
  else if (key === "rename") onRename?.();
@@ -2403,39 +3447,39 @@ function ChatTitleMenu({ pinned, onPin, onRename, projects, onAddToProject, onCr
2403
3447
  else if (key === "delete") onDelete?.();
2404
3448
  };
2405
3449
  return /* @__PURE__ */ jsxs(Menu, {
2406
- "aria-label": "Chat actions",
3450
+ "aria-label": labels.chatActions,
2407
3451
  className: "min-w-44",
2408
3452
  onAction: act,
2409
3453
  children: [
2410
3454
  onPin && /* @__PURE__ */ jsx(MenuItem, {
2411
3455
  id: "pin",
2412
- icon: pinned ? /* @__PURE__ */ jsx(PinOff, {}) : /* @__PURE__ */ jsx(Pin, {}),
3456
+ icon: pinned ? icons.unpin : icons.pin,
2413
3457
  shortcut: "P",
2414
- children: pinned ? "Unpin" : "Pin"
3458
+ children: pinned ? labels.unpin : labels.pin
2415
3459
  }),
2416
3460
  onRename && /* @__PURE__ */ jsx(MenuItem, {
2417
3461
  id: "rename",
2418
- icon: /* @__PURE__ */ jsx(Pencil, {}),
3462
+ icon: icons.rename,
2419
3463
  shortcut: "R",
2420
- children: "Rename"
3464
+ children: labels.rename
2421
3465
  }),
2422
3466
  onAddToProject && /* @__PURE__ */ jsxs(SubmenuTrigger, { children: [/* @__PURE__ */ jsx(MenuItem, {
2423
3467
  id: "project",
2424
- icon: /* @__PURE__ */ jsx(FolderPlus, {}),
2425
- children: "Add to project"
3468
+ icon: icons.addToProject,
3469
+ children: labels.addToProject
2426
3470
  }), /* @__PURE__ */ jsx(Popover, {
2427
3471
  placement: "end top",
2428
3472
  className: "w-60",
2429
3473
  children: /* @__PURE__ */ jsx(MenuSearch, {
2430
- placeholder: "Search or create a project",
3474
+ placeholder: labels.searchProjects,
2431
3475
  children: /* @__PURE__ */ jsxs(Menu, {
2432
- "aria-label": "Projects",
3476
+ "aria-label": labels.projects,
2433
3477
  className: "max-h-72",
2434
3478
  onAction: (key) => key === "__new" ? onCreateProject?.() : onAddToProject(String(key)),
2435
3479
  children: [onCreateProject && /* @__PURE__ */ jsx(MenuItem, {
2436
3480
  id: "__new",
2437
- icon: /* @__PURE__ */ jsx(Plus, {}),
2438
- children: "New project"
3481
+ icon: icons.add,
3482
+ children: labels.newProject
2439
3483
  }), (projects ?? []).map((p) => /* @__PURE__ */ jsx(MenuItem, {
2440
3484
  id: p.id,
2441
3485
  children: p.name
@@ -2445,28 +3489,50 @@ function ChatTitleMenu({ pinned, onPin, onRename, projects, onAddToProject, onCr
2445
3489
  })] }),
2446
3490
  onShare && /* @__PURE__ */ jsx(MenuItem, {
2447
3491
  id: "share",
2448
- icon: /* @__PURE__ */ jsx(Share, {}),
2449
- children: "Share chat"
3492
+ icon: icons.share,
3493
+ children: labels.shareChat
2450
3494
  }),
2451
3495
  children,
2452
3496
  onDelete && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MenuSeparator, {}), /* @__PURE__ */ jsx(MenuItem, {
2453
3497
  id: "delete",
2454
- icon: /* @__PURE__ */ jsx(Trash2, {}),
3498
+ icon: icons.delete,
2455
3499
  shortcut: "D",
2456
3500
  tone: "danger",
2457
- children: "Delete"
3501
+ children: labels.delete
2458
3502
  })] })
2459
3503
  ]
2460
3504
  });
2461
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
+ }
2462
3526
  // src/components/chat-list.tsx
2463
3527
  const isProject = (item) => "type" in item && item.type === "project";
2464
- 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");
2465
3529
  /**
2466
3530
  * The chat picker: sections of chats and projects, collapsible, with an active row, running and unread
2467
3531
  * markers and a per-row menu. Usable on its own (in a host's own sidebar) or inside `Sidebar`.
2468
3532
  */
2469
3533
  function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }) {
3534
+ const icons = useIcons();
3535
+ const labels = useLabels();
2470
3536
  const row = (chat, nested = false) => /* @__PURE__ */ jsx(ChatRow, {
2471
3537
  chat,
2472
3538
  active: chat.id === activeId,
@@ -2480,13 +3546,13 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
2480
3546
  row
2481
3547
  }, item.id) : row(item));
2482
3548
  return /* @__PURE__ */ jsx("nav", {
2483
- "aria-label": props["aria-label"] ?? "Chats",
3549
+ "aria-label": props["aria-label"] ?? labels.chats,
2484
3550
  className: clsx("flex flex-col gap-3", className),
2485
3551
  children: sections.map((section) => section.title ? /* @__PURE__ */ jsxs(Disclosure$1, {
2486
3552
  defaultExpanded: section.defaultExpanded ?? true,
2487
3553
  className: "group/section",
2488
3554
  children: [/* @__PURE__ */ jsxs(Heading, {
2489
- 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",
2490
3556
  children: [
2491
3557
  /* @__PURE__ */ jsxs(Button$1, {
2492
3558
  slot: "trigger",
@@ -2494,9 +3560,9 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
2494
3560
  children: [/* @__PURE__ */ jsx("span", {
2495
3561
  className: "truncate",
2496
3562
  children: section.title
2497
- }), /* @__PURE__ */ jsx(ChevronRight, {
2498
- "aria-hidden": true,
2499
- 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
2500
3566
  })]
2501
3567
  }),
2502
3568
  section.badge,
@@ -2511,7 +3577,7 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
2511
3577
  }), section.onViewAll && /* @__PURE__ */ jsx(Button$1, {
2512
3578
  onPress: section.onViewAll,
2513
3579
  className: clsx(rowClass, "text-chat-muted"),
2514
- children: "View all"
3580
+ children: labels.viewAll
2515
3581
  })] })]
2516
3582
  }, section.id) : /* @__PURE__ */ jsx("ul", {
2517
3583
  className: "flex flex-col gap-px",
@@ -2520,16 +3586,18 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
2520
3586
  });
2521
3587
  }
2522
3588
  function ChatRow({ chat, active, nested, onSelect, menu }) {
3589
+ const icons = useIcons();
2523
3590
  const [menuOpen, setMenuOpen] = useState(false);
2524
3591
  const { dismiss } = useSidebar();
3592
+ const labels = useLabels();
2525
3593
  const markerClass = "size-2 shrink-0 rounded-full";
2526
3594
  const marker = chat.running ? /* @__PURE__ */ jsx("span", {
2527
3595
  role: "img",
2528
- "aria-label": "Running",
3596
+ "aria-label": labels.running,
2529
3597
  className: clsx(markerClass, "animate-pulse bg-chat-accent motion-reduce:animate-none")
2530
3598
  }) : chat.unread ? /* @__PURE__ */ jsx("span", {
2531
3599
  role: "img",
2532
- "aria-label": "Unread",
3600
+ "aria-label": labels.unread,
2533
3601
  className: clsx(markerClass, "bg-chat-check")
2534
3602
  }) : null;
2535
3603
  const content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
@@ -2554,11 +3622,11 @@ function ChatRow({ chat, active, nested, onSelect, menu }) {
2554
3622
  isOpen: menuOpen,
2555
3623
  onOpenChange: setMenuOpen,
2556
3624
  children: [/* @__PURE__ */ jsx(IconButton, {
2557
- label: `More options for ${chat.title}`,
3625
+ label: labels.moreOptionsFor(chat.title),
2558
3626
  size: "sm",
2559
3627
  tooltip: false,
2560
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"),
2561
- children: /* @__PURE__ */ jsx(MoreHorizontal, {})
3629
+ children: icons.more
2562
3630
  }), /* @__PURE__ */ jsx(Popover, {
2563
3631
  placement: "bottom start",
2564
3632
  children: menu
@@ -2567,6 +3635,8 @@ function ChatRow({ chat, active, nested, onSelect, menu }) {
2567
3635
  });
2568
3636
  }
2569
3637
  function ProjectRow({ project, activeId, row }) {
3638
+ const icons = useIcons();
3639
+ const labels = useLabels();
2570
3640
  const holdsActive = project.chats.some((c) => c.id === activeId);
2571
3641
  return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Disclosure$1, {
2572
3642
  defaultExpanded: project.defaultExpanded ?? holdsActive,
@@ -2576,18 +3646,15 @@ function ProjectRow({ project, activeId, row }) {
2576
3646
  slot: "trigger",
2577
3647
  className: rowClass,
2578
3648
  children: [
2579
- isExpanded ? /* @__PURE__ */ jsx(FolderOpen, {
2580
- "aria-hidden": true,
2581
- className: "size-4 shrink-0 text-chat-muted"
2582
- }) : /* @__PURE__ */ jsx(Folder, {
2583
- "aria-hidden": true,
2584
- 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
2585
3652
  }),
2586
3653
  /* @__PURE__ */ jsx("span", {
2587
3654
  className: "min-w-0 flex-1 truncate",
2588
3655
  children: project.name
2589
3656
  }),
2590
- !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 })
2591
3658
  ]
2592
3659
  })
2593
3660
  }), /* @__PURE__ */ jsx(DisclosurePanel, { children: /* @__PURE__ */ jsx("ul", {
@@ -2597,7 +3664,11 @@ function ProjectRow({ project, activeId, row }) {
2597
3664
  }) });
2598
3665
  }
2599
3666
  // src/views/chat-view.tsx
2600
- /** 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
+ */
2601
3672
  const densityVars = {
2602
3673
  full: {},
2603
3674
  panel: {
@@ -2612,14 +3683,20 @@ const densityVars = {
2612
3683
  }
2613
3684
  };
2614
3685
  /** A whole chat: header, the conversation, the docked composer. An empty chat shows `emptyState`. */
2615
- 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 }) {
2616
3687
  const style = {
2617
3688
  ...densityVars[density],
2618
3689
  ...contentWidth && { "--chat-content-width": contentWidth },
2619
3690
  ...contentPadding && { "--chat-content-padding": contentPadding }
2620
3691
  };
3692
+ const host = useMemo(() => ({
3693
+ onPrompt: onSend,
3694
+ onOpenLink: openInNewTab,
3695
+ ...widgets
3696
+ }), [onSend, widgets]);
2621
3697
  const empty = messages.length === 0;
2622
3698
  const [body, short] = useShortPane();
3699
+ const labels = useLabels();
2623
3700
  const pending = onApprove && approvals?.[0];
2624
3701
  const box = (variant) => /* @__PURE__ */ jsx(Composer, {
2625
3702
  variant: density === "inline" || short && !empty ? "compact" : variant,
@@ -2627,7 +3704,7 @@ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove
2627
3704
  onSubmit: onSend,
2628
3705
  ...onStop && { onStop },
2629
3706
  ...composer,
2630
- ...pending && { placeholder: "Or reply directly…" }
3707
+ ...pending && { placeholder: labels.replyDirectly }
2631
3708
  });
2632
3709
  return /* @__PURE__ */ jsxs("div", {
2633
3710
  "data-chat-density": density,
@@ -2642,28 +3719,31 @@ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove
2642
3719
  className: "w-full max-w-(--chat-content-width)",
2643
3720
  children: emptyState ? emptyState(box("centered")) : box("centered")
2644
3721
  })
2645
- }) : /* @__PURE__ */ jsx(MessageList, {
2646
- messages,
2647
- ...onRetry && { onRetry },
2648
- ...onEdit && { onEdit },
2649
- ...onFeedback && { onFeedback },
2650
- ...renderPart && { renderPart },
2651
- footer: /* @__PURE__ */ jsx("div", {
2652
- className: clsx("bg-linear-to-t from-chat-bg from-70% to-transparent px-(--chat-gutter)", short ? "pt-2" : "pt-4"),
2653
- children: /* @__PURE__ */ jsxs("div", {
2654
- className: "mx-auto w-full max-w-(--chat-content-width)",
2655
- children: [
2656
- pending && /* @__PURE__ */ jsx(ApprovalCard, {
2657
- approval: pending,
2658
- onRespond: onApprove,
2659
- className: "mb-2"
2660
- }, pending.part.id),
2661
- box("docked"),
2662
- /* @__PURE__ */ jsx("div", {
2663
- 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"),
2664
- children: disclaimer && !short && /* @__PURE__ */ jsx("span", { children: disclaimer })
2665
- })
2666
- ]
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
+ })
2667
3747
  })
2668
3748
  })
2669
3749
  })
@@ -2700,6 +3780,51 @@ function EmptyState({ greeting, icon, children, footer }) {
2700
3780
  ]
2701
3781
  });
2702
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
+ }
2703
3828
  // src/views/app-shell.tsx
2704
3829
  /**
2705
3830
  * A page with a sidebar: pinned and collapsible to an icon rail from tablet width up, a drawer over the
@@ -2724,6 +3849,7 @@ function AppShell({ sidebar, children, defaultCollapsed, className }) {
2724
3849
  toggle,
2725
3850
  dismiss
2726
3851
  ]);
3852
+ const labels = useLabels();
2727
3853
  const inDrawer = useMemo(() => ({
2728
3854
  ...pinned,
2729
3855
  collapsed: false
@@ -2742,7 +3868,7 @@ function AppShell({ sidebar, children, defaultCollapsed, className }) {
2742
3868
  }), drawer && /* @__PURE__ */ jsx(Drawer, {
2743
3869
  isOpen: drawerOpen,
2744
3870
  onOpenChange: setDrawerOpen,
2745
- "aria-label": "Sidebar",
3871
+ "aria-label": labels.sidebar,
2746
3872
  children: /* @__PURE__ */ jsx(SidebarContext.Provider, {
2747
3873
  value: inDrawer,
2748
3874
  children: sidebar
@@ -2752,13 +3878,16 @@ function AppShell({ sidebar, children, defaultCollapsed, className }) {
2752
3878
  }
2753
3879
  /** Opens the drawer on phones, collapses / expands the pinned sidebar elsewhere. */
2754
3880
  function SidebarToggle({ drawerOnly = false, className }) {
3881
+ const icons = useIcons();
2755
3882
  const { drawer, drawerOpen, collapsed, toggle } = useSidebar();
3883
+ const labels = useLabels();
2756
3884
  if (drawerOnly && !drawer) return null;
3885
+ const label = drawer ? drawerOpen ? labels.closeSidebar : labels.openSidebar : collapsed ? labels.expandSidebar : labels.collapseSidebar;
2757
3886
  return /* @__PURE__ */ jsx(IconButton, {
2758
- label: drawer ? drawerOpen ? "Close sidebar" : "Open sidebar" : collapsed ? "Expand sidebar" : "Collapse sidebar",
3887
+ label,
2759
3888
  onPress: toggle,
2760
3889
  className,
2761
- children: /* @__PURE__ */ jsx(PanelLeft, {})
3890
+ children: icons.sidebar
2762
3891
  });
2763
3892
  }
2764
3893
  // src/views/sidebar.tsx
@@ -2798,7 +3927,7 @@ function Sidebar({ brand, headerActions, nav, children, footer, className }) {
2798
3927
  /** A nav row: icon and label; just the icon with a tooltip when collapsed. Closes the drawer when picked. */
2799
3928
  function SidebarItem({ icon, label, onPress, href, active, shortcut }) {
2800
3929
  const { collapsed, dismiss } = useSidebar();
2801
- 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");
2802
3931
  const press = () => {
2803
3932
  onPress?.();
2804
3933
  dismiss();
@@ -2813,7 +3942,7 @@ function SidebarItem({ icon, label, onPress, href, active, shortcut }) {
2813
3942
  children: label
2814
3943
  }),
2815
3944
  !collapsed && shortcut && /* @__PURE__ */ jsx("kbd", {
2816
- 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",
2817
3946
  children: shortcut
2818
3947
  })
2819
3948
  ] });
@@ -2839,6 +3968,7 @@ function SidebarItem({ icon, label, onPress, href, active, shortcut }) {
2839
3968
  const initials = (name) => name.split(/\s+/).slice(0, 2).map((w) => w[0]?.toUpperCase() ?? "").join("");
2840
3969
  /** The account row at the bottom: avatar, name, plan, and a menu. */
2841
3970
  function SidebarUser({ name, detail, avatarUrl, menu }) {
3971
+ const icons = useIcons();
2842
3972
  const { collapsed } = useSidebar();
2843
3973
  const avatar = avatarUrl ? /* @__PURE__ */ jsx("img", {
2844
3974
  src: avatarUrl,
@@ -2846,7 +3976,7 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
2846
3976
  className: "size-8 shrink-0 rounded-full object-cover"
2847
3977
  }) : /* @__PURE__ */ jsx("span", {
2848
3978
  "aria-hidden": true,
2849
- 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",
2850
3980
  children: initials(name)
2851
3981
  });
2852
3982
  const button = /* @__PURE__ */ jsxs(Button$1, {
@@ -2855,15 +3985,15 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
2855
3985
  children: [avatar, !collapsed && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
2856
3986
  className: "flex min-w-0 flex-1 flex-col",
2857
3987
  children: [/* @__PURE__ */ jsx("span", {
2858
- className: "truncate text-sm text-chat-fg",
3988
+ className: "truncate text-(length:--chat-ui-font-size) leading-5 text-chat-fg",
2859
3989
  children: name
2860
3990
  }), detail && /* @__PURE__ */ jsx("span", {
2861
- className: "truncate text-xs text-chat-muted",
3991
+ className: "truncate text-(length:--chat-ui-font-size-sm) leading-4 text-chat-muted",
2862
3992
  children: detail
2863
3993
  })]
2864
- }), menu && /* @__PURE__ */ jsx(ChevronsUpDown, {
2865
- "aria-hidden": true,
2866
- 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
2867
3997
  })] })]
2868
3998
  });
2869
3999
  if (!menu) return button;
@@ -2872,4 +4002,4 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
2872
4002
  children: menu
2873
4003
  })] });
2874
4004
  }
2875
- 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 };