agentic-ui-kit 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -2
  3. package/dist/core.js +1 -0
  4. package/dist/index.js +2785 -0
  5. package/dist/sse-BA_ebMcH.js +408 -0
  6. package/dist/styles.css +3 -0
  7. package/dist/tailwind.css +333 -0
  8. package/dist/types/core/src/blocks.d.ts +22 -0
  9. package/dist/types/core/src/events.d.ts +75 -0
  10. package/dist/types/core/src/index.d.ts +7 -0
  11. package/dist/types/core/src/partial-json.d.ts +7 -0
  12. package/dist/types/core/src/reduce.d.ts +33 -0
  13. package/dist/types/core/src/sse.d.ts +8 -0
  14. package/dist/types/core/src/types.d.ts +171 -0
  15. package/dist/types/react/src/components/activity-group.d.ts +13 -0
  16. package/dist/types/react/src/components/approval-card.d.ts +11 -0
  17. package/dist/types/react/src/components/attachments.d.ts +78 -0
  18. package/dist/types/react/src/components/chat-header.d.ts +30 -0
  19. package/dist/types/react/src/components/chat-list.d.ts +47 -0
  20. package/dist/types/react/src/components/composer.d.ts +45 -0
  21. package/dist/types/react/src/components/context-panel.d.ts +13 -0
  22. package/dist/types/react/src/components/markdown.d.ts +8 -0
  23. package/dist/types/react/src/components/message-list.d.ts +18 -0
  24. package/dist/types/react/src/components/messages.d.ts +33 -0
  25. package/dist/types/react/src/components/model-picker.d.ts +16 -0
  26. package/dist/types/react/src/components/token-counter.d.ts +16 -0
  27. package/dist/types/react/src/components/tools.d.ts +68 -0
  28. package/dist/types/react/src/components/word-fade.d.ts +19 -0
  29. package/dist/types/react/src/core.d.ts +1 -0
  30. package/dist/types/react/src/hooks/agent.d.ts +86 -0
  31. package/dist/types/react/src/hooks/sidebar-state.d.ts +18 -0
  32. package/dist/types/react/src/hooks/smooth-text.d.ts +9 -0
  33. package/dist/types/react/src/hooks/sse-transport.d.ts +15 -0
  34. package/dist/types/react/src/hooks/use-chat.d.ts +68 -0
  35. package/dist/types/react/src/index.d.ts +56 -0
  36. package/dist/types/react/src/primitives/badge.d.ts +15 -0
  37. package/dist/types/react/src/primitives/button.d.ts +32 -0
  38. package/dist/types/react/src/primitives/caret.d.ts +4 -0
  39. package/dist/types/react/src/primitives/chat-theme.d.ts +13 -0
  40. package/dist/types/react/src/primitives/code-block.d.ts +13 -0
  41. package/dist/types/react/src/primitives/context-menu.d.ts +13 -0
  42. package/dist/types/react/src/primitives/disclosure.d.ts +16 -0
  43. package/dist/types/react/src/primitives/highlight.d.ts +8 -0
  44. package/dist/types/react/src/primitives/menu.d.ts +52 -0
  45. package/dist/types/react/src/primitives/overlay.d.ts +21 -0
  46. package/dist/types/react/src/primitives/panel.d.ts +19 -0
  47. package/dist/types/react/src/primitives/tooltip.d.ts +10 -0
  48. package/dist/types/react/src/views/app-shell.d.ts +20 -0
  49. package/dist/types/react/src/views/chat-view.d.ts +51 -0
  50. package/dist/types/react/src/views/sidebar.d.ts +38 -0
  51. package/package.json +80 -3
package/dist/index.js ADDED
@@ -0,0 +1,2785 @@
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";
3
+ import { clsx } from "clsx";
4
+ import { Autocomplete, Button as Button$1, Dialog, Disclosure as Disclosure$1, DisclosurePanel, Focusable, Header, Heading, Input, Keyboard, Link, Menu as Menu$1, MenuItem as MenuItem$1, MenuSection as MenuSection$1, MenuTrigger, MenuTrigger as MenuTrigger$1, Modal as Modal$1, ModalOverlay, Popover as Popover$1, SearchField, Separator, SubmenuTrigger, Text, Tooltip as Tooltip$1, TooltipTrigger, useFilter } from "react-aria-components";
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";
8
+ import { UNSAFE_PortalProvider } from "react-aria";
9
+ import { createMathPlugin } from "@streamdown/math";
10
+ import { Streamdown, defaultRehypePlugins } from "streamdown";
11
+ // src/primitives/tooltip.tsx
12
+ function Tooltip({ content, children, placement = "top", delay = 600 }) {
13
+ return /* @__PURE__ */ jsxs(TooltipTrigger, {
14
+ delay,
15
+ closeDelay: 0,
16
+ children: [children, /* @__PURE__ */ jsx(Tooltip$1, {
17
+ placement,
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",
20
+ children: content
21
+ })]
22
+ });
23
+ }
24
+ // src/primitives/button.tsx
25
+ const focusRing = "outline-none data-focus-visible:ring-2 data-focus-visible:ring-chat-focus";
26
+ const variants = {
27
+ /** Plain text button, highlights on hover. Sidebar rows, header buttons. */
28
+ ghost: "text-chat-fg data-hovered:bg-chat-hover data-pressed:bg-chat-selected",
29
+ /** Filled with the surface color, e.g. the selected Chat/Cowork toggle. */
30
+ subtle: "bg-chat-surface text-chat-fg data-hovered:bg-chat-hover data-pressed:bg-chat-selected",
31
+ /** The send button. */
32
+ primary: "bg-chat-accent text-white data-hovered:brightness-110 data-pressed:brightness-95"
33
+ };
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"
37
+ };
38
+ function Button({ variant = "ghost", size = "md", className, ...props }) {
39
+ return /* @__PURE__ */ jsx(Button$1, {
40
+ ...props,
41
+ className: clsx("inline-flex cursor-pointer items-center rounded-chat-sm font-chat-sans whitespace-nowrap transition-colors select-none", "data-disabled:cursor-default data-disabled:opacity-40", focusRing, variants[variant], sizes[size], className)
42
+ });
43
+ }
44
+ function IconButton({ label, variant = "ghost", size = "md", tooltip = true, className, ...props }) {
45
+ const button = /* @__PURE__ */ jsx(Button$1, {
46
+ ...props,
47
+ "aria-label": label,
48
+ className: clsx("inline-flex shrink-0 cursor-pointer items-center justify-center rounded-chat-sm text-chat-muted transition-colors select-none", "data-hovered:text-chat-fg data-disabled:cursor-default data-disabled:opacity-40 [&_svg]:size-4", focusRing, variants[variant], variant === "ghost" && "text-chat-muted", size === "sm" ? "size-7 [@media(pointer:coarse)]:size-9" : "size-8 [@media(pointer:coarse)]:size-10", className)
49
+ });
50
+ return tooltip ? /* @__PURE__ */ jsx(Tooltip, {
51
+ content: label,
52
+ children: button
53
+ }) : button;
54
+ }
55
+ // src/primitives/badge.tsx
56
+ const tones = {
57
+ /** Outlined, e.g. "Recommended". */
58
+ outline: "border border-chat-border-strong text-chat-muted",
59
+ /** Amber, e.g. "5.5× or more usage". */
60
+ warning: "bg-chat-warning-bg text-chat-warning"
61
+ };
62
+ function Badge({ children, tone = "outline", icon, className }) {
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),
65
+ children: [icon, children]
66
+ });
67
+ }
68
+ // src/primitives/caret.tsx
69
+ /** The small filled triangle claude.ai uses after expandable labels. Points right; rotate to point down. */
70
+ function Caret({ className }) {
71
+ return /* @__PURE__ */ jsx("svg", {
72
+ "aria-hidden": true,
73
+ viewBox: "0 0 8 8",
74
+ className: clsx("size-2 shrink-0 fill-current transition-transform duration-150", className),
75
+ children: /* @__PURE__ */ jsx("path", { d: "M2 1.2 6.2 4 2 6.8z" })
76
+ });
77
+ }
78
+ // src/primitives/disclosure.tsx
79
+ /** A row that expands to show more, like "Recalled memory ▸". */
80
+ function Disclosure({ summary, children, className, summaryClassName, panelClassName, indicator = "caret", trailing, ...props }) {
81
+ return /* @__PURE__ */ jsxs(Disclosure$1, {
82
+ ...props,
83
+ ...className !== void 0 && { className },
84
+ children: [/* @__PURE__ */ jsx(Heading, {
85
+ className: "m-0 font-normal",
86
+ children: /* @__PURE__ */ jsxs(Button$1, {
87
+ slot: "trigger",
88
+ 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
+ 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"
93
+ }),
94
+ /* @__PURE__ */ jsx("span", {
95
+ className: "min-w-0 truncate",
96
+ children: summary
97
+ }),
98
+ indicator === "caret" && /* @__PURE__ */ jsx(Caret, { className: "group-aria-expanded/trigger:rotate-90" }),
99
+ trailing && /* @__PURE__ */ jsx("span", {
100
+ className: "ml-auto shrink-0 pl-2",
101
+ children: trailing
102
+ })
103
+ ]
104
+ })
105
+ }), /* @__PURE__ */ jsx(DisclosurePanel, { children: /* @__PURE__ */ jsx("div", {
106
+ className: panelClassName,
107
+ children
108
+ }) })]
109
+ });
110
+ }
111
+ // src/primitives/menu.tsx
112
+ /** A search box above a menu that filters its items, like "Search or create a project". */
113
+ function MenuSearch({ placeholder, children, value, onChange }) {
114
+ const { contains } = useFilter({ sensitivity: "base" });
115
+ return /* @__PURE__ */ jsxs(Autocomplete, {
116
+ filter: contains,
117
+ ...value !== void 0 && { inputValue: value },
118
+ ...onChange && { onInputChange: onChange },
119
+ children: [/* @__PURE__ */ jsx(SearchField, {
120
+ "aria-label": placeholder,
121
+ autoFocus: true,
122
+ className: "border-b border-chat-popover-border",
123
+ children: /* @__PURE__ */ jsx(Input, {
124
+ placeholder,
125
+ className: "w-full bg-transparent px-3.5 py-2.5 text-sm text-chat-fg outline-none placeholder:text-chat-subtle"
126
+ })
127
+ }), children]
128
+ });
129
+ }
130
+ /** Floating panel for menus and pickers: surface color, rounded, soft shadow, fades in. */
131
+ function Popover({ className, offset = 6, ...props }) {
132
+ return /* @__PURE__ */ jsx(Popover$1, {
133
+ offset,
134
+ ...props,
135
+ className: clsx("rounded-chat-popover border border-chat-popover-border bg-chat-popover font-chat-sans text-chat-fg shadow-chat-popover outline-none", "motion-safe:data-entering:animate-chat-in motion-safe:data-exiting:animate-chat-out", className)
136
+ });
137
+ }
138
+ function Menu({ className, ...props }) {
139
+ return /* @__PURE__ */ jsx(Menu$1, {
140
+ ...props,
141
+ className: clsx("max-h-[inherit] min-w-52 overflow-auto p-1.5 outline-none", className)
142
+ });
143
+ }
144
+ function MenuItem({ children, icon, description, detail, shortcut, tone = "default", className, ...props }) {
145
+ const textValue = props.textValue ?? (typeof children === "string" ? children : void 0);
146
+ return /* @__PURE__ */ jsx(MenuItem$1, {
147
+ ...props,
148
+ ...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),
150
+ children: ({ isSelected, hasSubmenu, selectionMode }) => /* @__PURE__ */ jsxs(Fragment, { children: [
151
+ icon && /* @__PURE__ */ jsx("span", {
152
+ className: clsx("flex size-4 shrink-0 items-center [&_svg]:size-4", tone === "danger" ? "text-chat-danger" : "text-chat-fg"),
153
+ children: icon
154
+ }),
155
+ /* @__PURE__ */ jsxs("span", {
156
+ className: "flex min-w-0 flex-1 flex-col",
157
+ children: [/* @__PURE__ */ jsx(Text, {
158
+ slot: "label",
159
+ className: "truncate",
160
+ children
161
+ }), description && /* @__PURE__ */ jsx(Text, {
162
+ slot: "description",
163
+ className: "text-xs text-chat-muted",
164
+ children: description
165
+ })]
166
+ }),
167
+ detail && /* @__PURE__ */ jsx("span", {
168
+ className: "shrink-0 text-sm text-chat-muted",
169
+ children: detail
170
+ }),
171
+ shortcut && /* @__PURE__ */ jsx(Keyboard, {
172
+ className: "shrink-0 font-chat-sans text-xs text-chat-subtle",
173
+ children: shortcut
174
+ }),
175
+ selectionMode !== "none" && isSelected && /* @__PURE__ */ jsx(Check, {
176
+ "aria-hidden": true,
177
+ className: "size-4 shrink-0 text-chat-check"
178
+ }),
179
+ hasSubmenu && /* @__PURE__ */ jsx(ChevronRight, {
180
+ "aria-hidden": true,
181
+ className: "size-4 shrink-0 text-chat-muted"
182
+ })
183
+ ] })
184
+ });
185
+ }
186
+ function MenuSection({ title, children, ...selection }) {
187
+ return /* @__PURE__ */ jsxs(MenuSection$1, {
188
+ ...selection,
189
+ className: "[&:not(:first-child)]:mt-1",
190
+ children: [title && /* @__PURE__ */ jsx(Header, {
191
+ className: "px-2 pt-1.5 pb-1 text-xs text-chat-subtle",
192
+ children: title
193
+ }), children]
194
+ });
195
+ }
196
+ function MenuSeparator() {
197
+ return /* @__PURE__ */ jsx(Separator, { className: "-mx-1.5 my-1.5 border-t border-chat-popover-border" });
198
+ }
199
+ /** Explanatory text inside a menu, above or below the items. Not focusable. */
200
+ function MenuNote({ children, className }) {
201
+ return /* @__PURE__ */ jsx("div", {
202
+ className: clsx("px-2 py-1.5 text-sm leading-snug text-chat-muted", className),
203
+ children
204
+ });
205
+ }
206
+ // src/primitives/context-menu.tsx
207
+ /**
208
+ * Opens `menu` at the pointer on right-click, or at the focused element with the Menu key or Shift+F10.
209
+ * Long-press opens it where the browser fires `contextmenu` for touch (Android); give touch users a
210
+ * visible "…" button as well.
211
+ */
212
+ function ContextMenu({ menu, children, className }) {
213
+ const root = useRef(null);
214
+ const [at, setAt] = useState();
215
+ const openAt = (x, y) => {
216
+ const box = root.current?.getBoundingClientRect();
217
+ if (box) setAt({
218
+ x: x - box.left,
219
+ y: y - box.top
220
+ });
221
+ };
222
+ const onContextMenu = (e) => {
223
+ e.preventDefault();
224
+ openAt(e.clientX, e.clientY);
225
+ };
226
+ const onKeyDown = (e) => {
227
+ if (e.key !== "ContextMenu" && !(e.shiftKey && e.key === "F10")) return;
228
+ e.preventDefault();
229
+ const target = e.target.getBoundingClientRect();
230
+ openAt(target.left + 8, target.bottom);
231
+ };
232
+ return /* @__PURE__ */ jsxs("div", {
233
+ ref: root,
234
+ onContextMenu,
235
+ onKeyDown,
236
+ className: clsx("relative", className),
237
+ children: [children, /* @__PURE__ */ jsxs(MenuTrigger$1, {
238
+ isOpen: at !== void 0,
239
+ onOpenChange: (open) => !open && setAt(void 0),
240
+ children: [/* @__PURE__ */ jsx(Button$1, {
241
+ "aria-hidden": true,
242
+ excludeFromTabOrder: true,
243
+ className: "pointer-events-none absolute size-0 opacity-0",
244
+ style: {
245
+ left: at?.x ?? 0,
246
+ top: at?.y ?? 0
247
+ }
248
+ }), /* @__PURE__ */ jsx(Popover, {
249
+ placement: "bottom start",
250
+ offset: 2,
251
+ children: menu
252
+ })]
253
+ })]
254
+ });
255
+ }
256
+ // src/primitives/overlay.tsx
257
+ /** A centered dialog over a dimmed page. Escape and clicking outside close it; focus stays inside. */
258
+ function Modal({ isOpen, onOpenChange, title, children, className, ...props }) {
259
+ return /* @__PURE__ */ jsx(ModalOverlay, {
260
+ isOpen,
261
+ onOpenChange,
262
+ 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",
264
+ children: /* @__PURE__ */ jsx(Modal$1, {
265
+ 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
+ children: /* @__PURE__ */ jsxs(Dialog, {
267
+ ...props,
268
+ className: "flex min-h-0 flex-1 flex-col outline-none",
269
+ children: [title && /* @__PURE__ */ jsx(Heading, {
270
+ slot: "title",
271
+ className: "px-5 pt-4 pb-3 text-base font-medium",
272
+ children: title
273
+ }), children]
274
+ })
275
+ })
276
+ });
277
+ }
278
+ /** A panel sliding in from the side over a dimmed page, e.g. the sidebar on phones. */
279
+ function Drawer({ isOpen, onOpenChange, side = "left", children, className, ...props }) {
280
+ return /* @__PURE__ */ jsx(ModalOverlay, {
281
+ isOpen,
282
+ onOpenChange,
283
+ isDismissable: true,
284
+ className: "fixed inset-0 z-40 bg-black/50 data-entering:animate-chat-fade-in data-exiting:animate-chat-fade-out",
285
+ children: /* @__PURE__ */ jsx(Modal$1, {
286
+ 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
+ children: /* @__PURE__ */ jsx(Dialog, {
288
+ ...props,
289
+ className: "h-full outline-none",
290
+ children
291
+ })
292
+ })
293
+ });
294
+ }
295
+ // src/primitives/panel.tsx
296
+ /**
297
+ * A titled region that fills its container and lets the body scroll: a dock panel, an agent pane in a
298
+ * grid, a settings card. A `ChatView` placed in the body sizes itself to the panel.
299
+ */
300
+ function Panel({ title, actions, footer, children, variant = "card", className, ...props }) {
301
+ return /* @__PURE__ */ jsxs("section", {
302
+ "aria-label": props["aria-label"],
303
+ 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
+ children: [
305
+ (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",
307
+ children: [/* @__PURE__ */ jsx("div", {
308
+ className: "min-w-0 flex-1 truncate font-medium",
309
+ children: title
310
+ }), actions && /* @__PURE__ */ jsx("div", {
311
+ className: "flex shrink-0 items-center gap-0.5",
312
+ children: actions
313
+ })]
314
+ }),
315
+ /* @__PURE__ */ jsx("div", {
316
+ className: "min-h-0 flex-1 overflow-y-auto [scrollbar-width:thin]",
317
+ children
318
+ }),
319
+ footer && /* @__PURE__ */ jsx("footer", {
320
+ className: "shrink-0 border-t border-chat-divider px-3 py-2",
321
+ children: footer
322
+ })
323
+ ]
324
+ });
325
+ }
326
+ // src/primitives/highlight.ts
327
+ /**
328
+ * Shiki themes for code. Each token carries both colors as --shiki-light / --shiki-dark, and CSS picks one
329
+ * with light-dark() from the chat theme's color-scheme. Backgrounds are ignored; blocks use --chat-code-bg.
330
+ */
331
+ const THEMES = {
332
+ light: "github-light",
333
+ dark: "one-dark-pro"
334
+ };
335
+ let shiki;
336
+ const loadShiki = () => shiki ??= import("shiki");
337
+ const cache = /* @__PURE__ */ new Map();
338
+ async function highlight(code, language) {
339
+ const key = `${language}\n${code}`;
340
+ const hit = cache.get(key);
341
+ if (hit) return hit;
342
+ const { codeToTokens, bundledLanguages, bundledLanguagesAlias } = await loadShiki();
343
+ const lang = language.toLowerCase();
344
+ if (!(lang in bundledLanguages) && !(lang in bundledLanguagesAlias)) return void 0;
345
+ const { tokens } = await codeToTokens(code, {
346
+ lang,
347
+ themes: THEMES,
348
+ defaultColor: false
349
+ });
350
+ if (cache.size > 200) cache.clear();
351
+ cache.set(key, tokens);
352
+ return tokens;
353
+ }
354
+ /**
355
+ * Highlights `code` in the background. Returns the latest finished result, so streaming code keeps its
356
+ * colors while the next version is highlighted instead of flashing back to plain text.
357
+ */
358
+ function useHighlight(code, language) {
359
+ const [lines, setLines] = useState();
360
+ useEffect(() => {
361
+ if (!language) return setLines(void 0);
362
+ let cancelled = false;
363
+ highlight(code, language).then((result) => !cancelled && setLines(result), () => !cancelled && setLines(void 0));
364
+ return () => {
365
+ cancelled = true;
366
+ };
367
+ }, [code, language]);
368
+ return lines;
369
+ }
370
+ // src/primitives/code-block.tsx
371
+ /** Monospace code with Shiki highlighting, scrolling past a fixed height. */
372
+ function CodeBlock({ code, language, label = language, variant = "plain", className }) {
373
+ const lines = useHighlight(code, language);
374
+ const done = lines?.map((l) => l.map((t) => t.content).join("")).join("\n");
375
+ const highlighted = lines && done !== void 0 && code.startsWith(done) ? lines : void 0;
376
+ const tail = highlighted ? code.slice(done.length) : "";
377
+ return /* @__PURE__ */ jsxs("div", {
378
+ "data-chat-code-block": true,
379
+ className: clsx(variant === "boxed" && "my-4 rounded-chat-md border border-chat-code-border bg-chat-code-bg px-4 py-3.5", className),
380
+ children: [label && /* @__PURE__ */ jsx("div", {
381
+ className: "mb-3 font-chat-sans text-xs text-chat-subtle",
382
+ children: label
383
+ }), /* @__PURE__ */ jsx("pre", {
384
+ 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]"),
385
+ 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", {
386
+ className: "chat-code-token",
387
+ style: token.htmlStyle,
388
+ children: token.content
389
+ }, j))] }, i)) : code, tail] })
390
+ })]
391
+ });
392
+ }
393
+ // src/primitives/chat-theme.tsx
394
+ /**
395
+ * Applies a chat theme to part of a page. Menus, tooltips and dialogs opened inside render into this
396
+ * element instead of `<body>`, so they get the same theme. An app themed as a whole (variables on
397
+ * `:root`) does not need it.
398
+ */
399
+ function ChatTheme({ theme, children, className }) {
400
+ const portal = useRef(null);
401
+ const getContainer = useCallback(() => portal.current, []);
402
+ return /* @__PURE__ */ jsxs("div", {
403
+ "data-chat-theme": theme,
404
+ className: clsx("bg-chat-bg font-chat-sans text-chat-fg", className),
405
+ children: [/* @__PURE__ */ jsx(UNSAFE_PortalProvider, {
406
+ getContainer,
407
+ children
408
+ }), /* @__PURE__ */ jsx("div", { ref: portal })]
409
+ });
410
+ }
411
+ // src/hooks/agent.ts
412
+ /**
413
+ * The tools and context the page currently offers. Components register what they own while mounted, so
414
+ * the model's tools follow what is on screen.
415
+ */
416
+ var AgentRegistry = class {
417
+ tools = /* @__PURE__ */ new Map();
418
+ contexts = /* @__PURE__ */ new Map();
419
+ listeners = /* @__PURE__ */ new Set();
420
+ version = 0;
421
+ addTool(holder) {
422
+ const name = holder.current.name;
423
+ this.tools.set(name, holder);
424
+ this.emit();
425
+ return () => {
426
+ if (this.tools.get(name) !== holder) return;
427
+ this.tools.delete(name);
428
+ this.emit();
429
+ };
430
+ }
431
+ setContext(key, value) {
432
+ this.contexts.set(key, value);
433
+ this.emit();
434
+ return () => {
435
+ if (this.contexts.get(key) !== value) return;
436
+ this.contexts.delete(key);
437
+ this.emit();
438
+ };
439
+ }
440
+ tool(name) {
441
+ return this.tools.get(name)?.current;
442
+ }
443
+ list() {
444
+ return [...this.tools.values()].map((h) => h.current);
445
+ }
446
+ /** Client tools, for the request. */
447
+ specs() {
448
+ return this.list().filter((t) => t.execute).map(({ name, description, inputSchema }) => ({
449
+ name,
450
+ ...description && { description },
451
+ ...inputSchema && { inputSchema }
452
+ }));
453
+ }
454
+ /** Everything the page shared with `useAgentContext`, for the request. */
455
+ context() {
456
+ return Object.fromEntries(this.contexts);
457
+ }
458
+ subscribe = (listener) => {
459
+ this.listeners.add(listener);
460
+ return () => this.listeners.delete(listener);
461
+ };
462
+ getVersion = () => this.version;
463
+ emit() {
464
+ this.version++;
465
+ for (const listener of this.listeners) listener();
466
+ }
467
+ };
468
+ const AgentContext = createContext(null);
469
+ /** Holds the tools and context for the chats and the page inside it. */
470
+ function AgentProvider({ registry, children }) {
471
+ const [own] = useState(() => registry ?? new AgentRegistry());
472
+ return createElement(AgentContext.Provider, { value: registry ?? own }, children);
473
+ }
474
+ const useAgentRegistry = () => useContext(AgentContext);
475
+ /** Re-renders when tools or context change, e.g. for an inspector. */
476
+ function useAgentSnapshot() {
477
+ const registry = useAgentRegistry();
478
+ useSyncExternalStore(registry?.subscribe ?? noopSubscribe, registry?.getVersion ?? zero, zero);
479
+ return registry;
480
+ }
481
+ const noopSubscribe = () => () => {};
482
+ const zero = () => 0;
483
+ /**
484
+ * Offers a tool while the calling component is mounted. The latest `tool` is always used, so `execute` can
485
+ * close over current state; it is only re-registered when the name changes.
486
+ */
487
+ function useAgentTool(tool) {
488
+ const registry = useAgentRegistry();
489
+ const holder = useRef(tool);
490
+ useLayoutEffect(() => {
491
+ holder.current = tool;
492
+ });
493
+ useEffect(() => registry?.addTool(holder), [registry, tool.name]);
494
+ }
495
+ /** Shares page state with the model, e.g. the form's current values. Sent with each request. */
496
+ function useAgentContext(key, value) {
497
+ const registry = useAgentRegistry();
498
+ useEffect(() => registry?.setContext(key, value), [
499
+ registry,
500
+ key,
501
+ value
502
+ ]);
503
+ }
504
+ // src/hooks/use-chat.ts
505
+ let counter$1 = 0;
506
+ const localId = () => `local_${Date.now().toString(36)}_${(counter$1++).toString(36)}`;
507
+ function userMessage(text, parentId, attachments = []) {
508
+ return {
509
+ id: localId(),
510
+ parentId,
511
+ role: "user",
512
+ status: "done",
513
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
514
+ parts: [...attachments, ...text.trim() ? [{
515
+ type: "text",
516
+ id: localId(),
517
+ text,
518
+ status: "done"
519
+ }] : []]
520
+ };
521
+ }
522
+ const wantsApproval = (part, tool) => part.approval === void 0 && (part.needsApproval === true || (typeof tool?.approval === "function" ? tool.approval(part.input) : tool?.approval === true));
523
+ const DECLINED = "Declined by the user";
524
+ /**
525
+ * A linear conversation driven by a Transport: sends, streams the reply through `applyEvent`, stops and
526
+ * retries. With an `AgentProvider` it also runs the agent loop: client tools run in the page, approvals
527
+ * wait for the user, then the reply continues; every finished tool call reaches its tool's `onResult`.
528
+ */
529
+ function useChat({ transport, conversationId, initialMessages = [], onTitle, agent, maxSteps = 12 }) {
530
+ const contextAgent = useAgentRegistry();
531
+ const registry = agent === void 0 ? contextAgent : agent;
532
+ const [messages, setMessages] = useState(initialMessages);
533
+ const [busy, setBusy] = useState(false);
534
+ const [approvals, setApprovals] = useState([]);
535
+ const run = useRef(null);
536
+ const latest = useRef({
537
+ messages,
538
+ transport,
539
+ conversationId,
540
+ onTitle,
541
+ registry
542
+ });
543
+ latest.current = {
544
+ messages,
545
+ transport,
546
+ conversationId,
547
+ onTitle,
548
+ registry
549
+ };
550
+ const loop = useRef({
551
+ options: {},
552
+ step: 0
553
+ });
554
+ const reported = useRef(null);
555
+ reported.current ??= new Set(initialMessages.flatMap((m) => m.parts.map((p) => p.id)));
556
+ const seen = useRef(/* @__PURE__ */ new Map());
557
+ const report = useCallback((message) => {
558
+ for (const part of message.parts) {
559
+ if (part.type !== "tool_call" || reported.current.has(part.id)) continue;
560
+ const tool = latest.current.registry?.tool(part.name);
561
+ try {
562
+ if (seen.current.get(part.id) !== part) {
563
+ seen.current.set(part.id, part);
564
+ tool?.onCall?.(part);
565
+ }
566
+ if (part.status === "done" || part.status === "error") {
567
+ reported.current.add(part.id);
568
+ seen.current.delete(part.id);
569
+ tool?.onResult?.(part);
570
+ }
571
+ } catch (error) {
572
+ console.error(`Tool callback for ${part.name} failed`, error);
573
+ }
574
+ }
575
+ }, []);
576
+ /** Puts a changed message into the list and the ref, so the next step sees it at once. */
577
+ const commit = useCallback((message) => {
578
+ const list = latest.current.messages;
579
+ const index = list.findIndex((m) => m.id === message.id);
580
+ const next = index === -1 ? [...list, message] : list.with(index, message);
581
+ latest.current.messages = next;
582
+ setMessages(next);
583
+ report(message);
584
+ }, [report]);
585
+ const finish = useCallback((reply, status, error) => {
586
+ if (!reply || reply.status !== "streaming") return;
587
+ const ended = applyEvent(reply, {
588
+ type: "run.end",
589
+ runId: reply.runId ?? "",
590
+ seq: (reply.lastSeq ?? 0) + 1,
591
+ status,
592
+ ...error !== void 0 && { error }
593
+ });
594
+ if (ended) commit(ended);
595
+ }, [commit]);
596
+ const advanceRef = useRef(() => {});
597
+ const start = useCallback((history, options, continuation) => {
598
+ const message = history.at(-1);
599
+ const controller = new AbortController();
600
+ const current = {
601
+ controller,
602
+ ...continuation && { reply: continuation }
603
+ };
604
+ run.current = current;
605
+ latest.current.messages = history;
606
+ setMessages(history);
607
+ setBusy(true);
608
+ let followUp = false;
609
+ (async () => {
610
+ try {
611
+ const { transport, conversationId, registry } = latest.current;
612
+ const tools = registry?.specs();
613
+ for await (const event of transport.send({
614
+ message,
615
+ history,
616
+ signal: controller.signal,
617
+ ...continuation && { continuation: true },
618
+ ...tools?.length && { tools },
619
+ ...registry && { context: registry.context() },
620
+ ...conversationId !== void 0 && { conversationId },
621
+ ...options.model !== void 0 && { model: options.model },
622
+ ...options.effort !== void 0 && { effort: options.effort }
623
+ })) {
624
+ if (controller.signal.aborted) break;
625
+ if (event.type === "title") {
626
+ latest.current.onTitle?.(event.title);
627
+ continue;
628
+ }
629
+ const next = applyEvent(current.reply, event);
630
+ if (!next || next === current.reply) continue;
631
+ current.reply = next;
632
+ current.runId = next.runId;
633
+ commit(next);
634
+ }
635
+ if (controller.signal.aborted) finish(current.reply, "stopped");
636
+ else if (current.reply?.status === "done" && awaitingToolCalls(current.reply).length > 0) followUp = true;
637
+ } catch (error) {
638
+ if (controller.signal.aborted) finish(current.reply, "stopped");
639
+ else finish(current.reply, "error", error instanceof Error ? error.message : String(error));
640
+ } finally {
641
+ if (run.current === current) {
642
+ run.current = null;
643
+ if (followUp) advanceRef.current(current.reply);
644
+ else setBusy(false);
645
+ }
646
+ }
647
+ })();
648
+ }, [commit, finish]);
649
+ /** After a run ends with awaiting calls: ask, or run the client tools and continue. */
650
+ const advance = useCallback((reply) => {
651
+ const { registry } = latest.current;
652
+ const awaiting = awaitingToolCalls(reply);
653
+ const ask = awaiting.filter((p) => wantsApproval(p, registry?.tool(p.name)));
654
+ if (ask.length > 0) {
655
+ setApprovals(ask.map((part) => ({
656
+ messageId: reply.id,
657
+ part
658
+ })));
659
+ setBusy(false);
660
+ return;
661
+ }
662
+ const controller = new AbortController();
663
+ const current = {
664
+ controller,
665
+ reply
666
+ };
667
+ run.current = current;
668
+ setBusy(true);
669
+ (async () => {
670
+ let message = reply;
671
+ const set = (part, fields) => {
672
+ message = settleToolCall(message, part.id, fields);
673
+ current.reply = message;
674
+ commit(message);
675
+ };
676
+ for (const part of awaiting) {
677
+ if (controller.signal.aborted) break;
678
+ if (part.approval === "denied") set(part, {
679
+ status: "error",
680
+ error: DECLINED
681
+ });
682
+ else if (part.runsOn === "client") {
683
+ const tool = latest.current.registry?.tool(part.name);
684
+ if (!tool?.execute) {
685
+ set(part, {
686
+ status: "error",
687
+ error: `This page has no handler for ${part.name}`
688
+ });
689
+ continue;
690
+ }
691
+ set(part, { status: "running" });
692
+ const began = performance.now();
693
+ try {
694
+ set(part, {
695
+ status: "done",
696
+ result: await tool.execute(part.input, {
697
+ callId: part.id,
698
+ messageId: message.id,
699
+ signal: controller.signal
700
+ }),
701
+ durationMs: Math.round(performance.now() - began)
702
+ });
703
+ } catch (error) {
704
+ set(part, {
705
+ status: "error",
706
+ error: error instanceof Error ? error.message : String(error)
707
+ });
708
+ }
709
+ } else set(part, { status: "running" });
710
+ }
711
+ if (run.current !== current) return;
712
+ run.current = null;
713
+ if (controller.signal.aborted) {
714
+ for (const part of awaitingToolCalls(message).concat(message.parts.filter((p) => p.type === "tool_call" && p.status === "running"))) set(part, {
715
+ status: "error",
716
+ error: "Stopped"
717
+ });
718
+ setBusy(false);
719
+ return;
720
+ }
721
+ if (loop.current.step >= maxSteps) {
722
+ setBusy(false);
723
+ return;
724
+ }
725
+ loop.current.step++;
726
+ const list = latest.current.messages;
727
+ start(list.slice(0, list.findIndex((m) => m.id === message.id) + 1), loop.current.options, message);
728
+ })();
729
+ }, [
730
+ commit,
731
+ maxSteps,
732
+ start
733
+ ]);
734
+ advanceRef.current = advance;
735
+ const respond = useCallback((partId, granted) => {
736
+ const pending = approvals.find((a) => a.part.id === partId);
737
+ if (!pending) return;
738
+ const message = latest.current.messages.find((m) => m.id === pending.messageId);
739
+ if (!message) return;
740
+ const next = settleToolCall(message, partId, { approval: granted ? "granted" : "denied" });
741
+ commit(next);
742
+ const rest = approvals.filter((a) => a.part.id !== partId);
743
+ setApprovals(rest);
744
+ if (rest.length === 0) advance(next);
745
+ }, [
746
+ approvals,
747
+ advance,
748
+ commit
749
+ ]);
750
+ /** Declines whatever is waiting, e.g. because the user moved on. */
751
+ const declineAll = useCallback(() => {
752
+ for (const { messageId, part } of approvals) {
753
+ const message = latest.current.messages.find((m) => m.id === messageId);
754
+ if (message) commit(settleToolCall(message, part.id, {
755
+ approval: "denied",
756
+ status: "error",
757
+ error: DECLINED
758
+ }));
759
+ }
760
+ for (const message of latest.current.messages) for (const part of awaitingToolCalls(message)) commit(settleToolCall(message, part.id, {
761
+ status: "error",
762
+ error: DECLINED
763
+ }));
764
+ setApprovals([]);
765
+ }, [approvals, commit]);
766
+ const send = useCallback((text, options = {}) => {
767
+ const attachments = options.attachments ?? [];
768
+ if (run.current || !text.trim() && attachments.length === 0) return;
769
+ declineAll();
770
+ loop.current = {
771
+ options,
772
+ step: 0
773
+ };
774
+ const { messages } = latest.current;
775
+ start([...messages, userMessage(text, messages.at(-1)?.id ?? null, attachments)], options);
776
+ }, [declineAll, start]);
777
+ const stop = useCallback(() => {
778
+ const current = run.current;
779
+ if (!current) return declineAll();
780
+ current.controller.abort();
781
+ if (current.runId) latest.current.transport.stop?.(current.runId);
782
+ }, [declineAll]);
783
+ const retry = useCallback((assistantId, options = {}) => {
784
+ if (run.current) return;
785
+ const { messages } = latest.current;
786
+ const index = messages.findIndex((m) => m.id === assistantId);
787
+ if (index < 1 || messages[index - 1].role !== "user") return;
788
+ setApprovals([]);
789
+ loop.current = {
790
+ options,
791
+ step: 0
792
+ };
793
+ start(messages.slice(0, index), options);
794
+ }, [start]);
795
+ useEffect(() => () => run.current?.controller.abort(), []);
796
+ return {
797
+ messages,
798
+ busy,
799
+ approvals,
800
+ respond,
801
+ send,
802
+ stop,
803
+ retry
804
+ };
805
+ }
806
+ // src/hooks/sse-transport.ts
807
+ /** A `Transport` for backends that stream `StreamEvent`s over server-sent events (see `formatEvent`). */
808
+ function sseTransport({ url, headers, stopUrl, fetch: fetchImpl = fetch }) {
809
+ const headersFor = async () => ({
810
+ "content-type": "application/json",
811
+ ...Object.fromEntries(new Headers(typeof headers === "function" ? await headers() : headers))
812
+ });
813
+ return {
814
+ async *send({ signal, ...body }) {
815
+ const response = await fetchImpl(url, {
816
+ method: "POST",
817
+ headers: await headersFor(),
818
+ body: JSON.stringify(body),
819
+ signal
820
+ });
821
+ if (!response.ok || !response.body) throw new Error(`${url} answered ${response.status} ${response.statusText}`.trim());
822
+ yield* readEventStream(response.body);
823
+ },
824
+ ...stopUrl && { async stop(runId) {
825
+ await fetchImpl(stopUrl, {
826
+ method: "POST",
827
+ headers: await headersFor(),
828
+ body: JSON.stringify({ runId })
829
+ });
830
+ } }
831
+ };
832
+ }
833
+ // src/hooks/smooth-text.ts
834
+ /** Slowest reveal, in characters per second, so a trickle of tokens still reads as steady typing. */
835
+ const MIN_RATE = 45;
836
+ /** How many seconds of backlog the reveal aims to hold; bigger bursts speed it up. */
837
+ const LAG_SECONDS = .35;
838
+ /** Once the stream ends, the rest is revealed at least this fast. */
839
+ const FINISH_RATE = 600;
840
+ /** Text is released in groups this far apart, so each group of words fades in together. */
841
+ const CHUNK_MS = 90;
842
+ const prefersReducedMotion = () => typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
843
+ /**
844
+ * Paces streamed text so it appears at an even rate instead of in network-sized bursts. The reveal speed
845
+ * follows the backlog: it never falls far behind and never stalls. It stops at word ends so a word fades
846
+ * in whole. When `streaming` ends the rest catches up quickly. With reduced motion it returns `text`.
847
+ *
848
+ * One animation loop runs while there is text left to show; new text only moves its target, so frequent
849
+ * small deltas do not restart it. Text is released in groups every CHUNK_MS rather than every frame.
850
+ */
851
+ function useSmoothText(text, streaming) {
852
+ const [shown, setShown] = useState(() => streaming ? "" : text);
853
+ const s = useRef({
854
+ shown,
855
+ target: text,
856
+ streaming,
857
+ frame: 0,
858
+ last: 0,
859
+ carry: 0,
860
+ lastCommit: 0
861
+ }).current;
862
+ s.target = text;
863
+ s.streaming = streaming;
864
+ useEffect(() => {
865
+ if (prefersReducedMotion() || !text.startsWith(s.shown)) {
866
+ cancelAnimationFrame(s.frame);
867
+ s.frame = 0;
868
+ s.shown = text;
869
+ setShown(text);
870
+ return;
871
+ }
872
+ if (s.frame || s.shown.length >= text.length) return;
873
+ s.last = performance.now();
874
+ const step = (now) => {
875
+ const dt = Math.min(Math.max(now - s.last, 0), 100) / 1e3;
876
+ s.last = now;
877
+ const full = s.target;
878
+ const backlog = full.length - s.shown.length;
879
+ const rate = s.streaming ? Math.max(MIN_RATE, backlog / LAG_SECONDS) : Math.max(FINISH_RATE, backlog * 4);
880
+ s.carry += rate * dt;
881
+ const advance = Math.floor(s.carry);
882
+ if (advance > 0 && now - s.lastCommit >= CHUNK_MS) {
883
+ s.lastCommit = now;
884
+ s.carry -= advance;
885
+ s.shown = full.slice(0, wordEnd(full, Math.min(s.shown.length + advance, full.length)));
886
+ setShown(s.shown);
887
+ }
888
+ s.frame = s.shown.length < full.length ? requestAnimationFrame(step) : 0;
889
+ };
890
+ s.frame = requestAnimationFrame(step);
891
+ }, [
892
+ text,
893
+ streaming,
894
+ s
895
+ ]);
896
+ useEffect(() => () => cancelAnimationFrame(s.frame), [s]);
897
+ return shown;
898
+ }
899
+ /** Moves `index` forward to the end of the word it falls in, so words are never cut in half. */
900
+ function wordEnd(text, index) {
901
+ let i = index;
902
+ while (i < text.length && !/\s/.test(text[i])) i++;
903
+ return i;
904
+ }
905
+ // src/hooks/sidebar-state.ts
906
+ /** Below this width the sidebar is a drawer over the content (phones). */
907
+ const DRAWER_QUERY = "(max-width: 767.98px)";
908
+ function useMediaQuery(query) {
909
+ return useSyncExternalStore((notify) => {
910
+ const list = window.matchMedia(query);
911
+ list.addEventListener("change", notify);
912
+ return () => list.removeEventListener("change", notify);
913
+ }, () => window.matchMedia(query).matches, () => false);
914
+ }
915
+ const SidebarContext = createContext({
916
+ collapsed: false,
917
+ drawer: false,
918
+ drawerOpen: false,
919
+ toggle: () => {},
920
+ dismiss: () => {}
921
+ });
922
+ const useSidebar = () => useContext(SidebarContext);
923
+ // src/components/word-fade.ts
924
+ const OPAQUE_TAGS = /* @__PURE__ */ new Set([
925
+ "code",
926
+ "pre",
927
+ "svg",
928
+ "math",
929
+ "script",
930
+ "style"
931
+ ]);
932
+ function classes(node) {
933
+ const value = node.properties?.className;
934
+ if (Array.isArray(value)) return value.map(String);
935
+ return typeof value === "string" ? value.split(/\s+/) : [];
936
+ }
937
+ function isMath(node) {
938
+ return classes(node).some((c) => c === "katex" || c === "katex-display" || c.startsWith("math-") || c === "language-math");
939
+ }
940
+ function wordSpan(word) {
941
+ return {
942
+ type: "element",
943
+ tagName: "span",
944
+ properties: { className: ["chat-word"] },
945
+ children: [{
946
+ type: "text",
947
+ value: word
948
+ }]
949
+ };
950
+ }
951
+ function wrap(node) {
952
+ if (!node.children) return;
953
+ const out = [];
954
+ for (const child of node.children) {
955
+ if (child.type === "text") {
956
+ for (const piece of (child.value ?? "").split(/(\s+)/)) {
957
+ if (!piece) continue;
958
+ out.push(/^\s+$/.test(piece) ? {
959
+ type: "text",
960
+ value: piece
961
+ } : wordSpan(piece));
962
+ }
963
+ continue;
964
+ }
965
+ if (child.type === "element") {
966
+ if (isMath(child)) child.properties = {
967
+ ...child.properties,
968
+ className: [...classes(child), "chat-word"]
969
+ };
970
+ else if (!OPAQUE_TAGS.has(child.tagName ?? "")) wrap(child);
971
+ }
972
+ out.push(child);
973
+ }
974
+ node.children = out;
975
+ }
976
+ function rehypeWordFade() {
977
+ return (tree) => wrap(tree);
978
+ }
979
+ // src/components/markdown.tsx
980
+ const fadePlugins = [...Object.values(defaultRehypePlugins), rehypeWordFade];
981
+ const math = createMathPlugin({ singleDollarTextMath: true });
982
+ /** Streamdown's own utility classes get this prefix, so neither they nor the host's classes apply. */
983
+ const PREFIX = "llmchat";
984
+ function textOf(node) {
985
+ if (!node) return "";
986
+ if (node.type === "text") return node.value ?? "";
987
+ return (node.children ?? []).map(textOf).join("");
988
+ }
989
+ const components = {
990
+ pre: ({ children }) => /* @__PURE__ */ jsx(Fragment, { children }),
991
+ code: ({ node, className, children }) => {
992
+ const hast = node;
993
+ const language = /language-([\w+#-]+)/.exec(className ?? "")?.[1];
994
+ const multiline = hast?.position && hast.position.start.line !== hast.position.end.line;
995
+ if (!language && !multiline) return /* @__PURE__ */ jsx("code", {
996
+ "data-chat-inline-code": true,
997
+ children
998
+ });
999
+ return /* @__PURE__ */ jsx(CodeBlock, {
1000
+ variant: "boxed",
1001
+ code: textOf(hast).replace(/\n$/, ""),
1002
+ ...language && { language }
1003
+ });
1004
+ }
1005
+ };
1006
+ /** Message markdown: GFM, KaTeX math ($…$ and $$…$$), Shiki code blocks, smooth fade-in while streaming. */
1007
+ function Markdown({ children, streaming = false, className }) {
1008
+ const text = useSmoothText(children, streaming);
1009
+ const animating = streaming || text.length < children.length;
1010
+ const [streamed, setStreamed] = useState(streaming);
1011
+ if (streaming && !streamed) setStreamed(true);
1012
+ return /* @__PURE__ */ jsx("div", {
1013
+ className: clsx("chat-markdown", streamed && "chat-fade", className),
1014
+ children: /* @__PURE__ */ jsx(MarkdownBody, {
1015
+ text,
1016
+ animating,
1017
+ fade: streamed
1018
+ })
1019
+ });
1020
+ }
1021
+ const MarkdownBody = memo(function MarkdownBody({ text, animating, fade }) {
1022
+ return /* @__PURE__ */ jsx(Streamdown, {
1023
+ prefix: PREFIX,
1024
+ mode: animating || fade ? "streaming" : "static",
1025
+ ...fade && { rehypePlugins: fadePlugins },
1026
+ plugins: { math },
1027
+ components,
1028
+ controls: false,
1029
+ lineNumbers: false,
1030
+ linkSafety: { enabled: false },
1031
+ children: text
1032
+ });
1033
+ });
1034
+ // src/components/tools.tsx
1035
+ const ToolViewsContext = createContext(void 0);
1036
+ function ToolViewsProvider({ tools, children }) {
1037
+ return /* @__PURE__ */ jsx(ToolViewsContext.Provider, {
1038
+ value: tools,
1039
+ children
1040
+ });
1041
+ }
1042
+ function useToolView(name) {
1043
+ return useToolViews()[name] ?? {};
1044
+ }
1045
+ /** The provider's views, plus views brought by tools the page registered (`ChatTool.view`). */
1046
+ function useToolViews() {
1047
+ const base = useContext(ToolViewsContext) ?? defaultToolViews;
1048
+ const registry = useAgentSnapshot();
1049
+ const version = registry?.getVersion() ?? 0;
1050
+ return useMemo(() => {
1051
+ const own = registry?.list().filter((t) => t.view !== void 0) ?? [];
1052
+ return own.length ? {
1053
+ ...base,
1054
+ ...Object.fromEntries(own.map((t) => [t.name, t.view]))
1055
+ } : base;
1056
+ }, [
1057
+ base,
1058
+ registry,
1059
+ version
1060
+ ]);
1061
+ }
1062
+ /** `useAgentTool` for tools that bring a view. */
1063
+ const useChatTool = useAgentTool;
1064
+ /** Streaming or running: the model or a tool is busy with it. */
1065
+ const isWorking = (part) => part.status !== "done" && part.status !== "error" && part.status !== "awaiting";
1066
+ /** Row label for any activity part. */
1067
+ function activityLabel(part, view) {
1068
+ if (part.type === "thinking") {
1069
+ if (part.title) return part.title;
1070
+ if (part.status !== "done") return "Thinking…";
1071
+ return part.durationMs ? `Thought for ${formatDuration(part.durationMs)}` : "Thought";
1072
+ }
1073
+ return view?.label?.(part) ?? part.title ?? part.name;
1074
+ }
1075
+ /**
1076
+ * Heading of an activity group. While live it follows the latest step. Once done it lists what the tools
1077
+ * did, in order ("Ran a command, searched the web"); a group of only thinking shows the first title.
1078
+ */
1079
+ function activityHeading(parts, views) {
1080
+ const waiting = parts.find((p) => p.type === "tool_call" && p.status === "awaiting");
1081
+ if (waiting) return /* @__PURE__ */ jsxs(Fragment, { children: [activityLabel(waiting, views[waiting.name]), " · waiting for you"] });
1082
+ if (parts.some(isWorking)) {
1083
+ const last = parts.at(-1);
1084
+ return activityLabel(last, last.type === "tool_call" ? views[last.name] : void 0);
1085
+ }
1086
+ const counts = /* @__PURE__ */ new Map();
1087
+ for (const part of parts) if (part.type === "tool_call") counts.set(part.name, (counts.get(part.name) ?? 0) + 1);
1088
+ if (counts.size === 0) return parts[0] ? activityLabel(parts[0]) : "";
1089
+ const text = [...counts].map(([name, count]) => views[name]?.summary?.(count) ?? `used ${name}`).join(", ");
1090
+ return text.charAt(0).toUpperCase() + text.slice(1);
1091
+ }
1092
+ const plural = (count, one, many) => count === 1 ? one : many(count);
1093
+ function recordOf(value) {
1094
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1095
+ }
1096
+ function stringOf(value) {
1097
+ return typeof value === "string" ? value : void 0;
1098
+ }
1099
+ /** A shell command. Input `{ command, description? }`; result a string or `{ stdout, stderr, exitCode }`. */
1100
+ const commandTool = {
1101
+ label: () => "Ran a command",
1102
+ hint: (part) => stringOf(recordOf(part.input).description),
1103
+ summary: (n) => plural(n, "ran a command", (n) => `ran ${n} commands`),
1104
+ Detail: function CommandDetail({ part }) {
1105
+ const command = stringOf(recordOf(part.input).command) ?? part.inputJson;
1106
+ const result = recordOf(part.result);
1107
+ const output = stringOf(part.result) ?? [stringOf(result.stdout), stringOf(result.stderr)].filter(Boolean).join("\n");
1108
+ return /* @__PURE__ */ jsxs("div", {
1109
+ className: "space-y-4",
1110
+ children: [
1111
+ /* @__PURE__ */ jsx(CodeBlock, {
1112
+ label: "bash",
1113
+ code: command
1114
+ }),
1115
+ (output || part.status === "done") && /* @__PURE__ */ jsx(CodeBlock, {
1116
+ label: "Output",
1117
+ code: output || "(no output)"
1118
+ }),
1119
+ part.error && /* @__PURE__ */ jsx("p", {
1120
+ className: "text-sm text-chat-danger",
1121
+ children: part.error
1122
+ })
1123
+ ]
1124
+ });
1125
+ }
1126
+ };
1127
+ /** A web search. Input `{ query }`; result `{ results: SearchResult[] }` or `SearchResult[]`. */
1128
+ const webSearchTool = {
1129
+ label: () => "Searched the web",
1130
+ subtitle: (part) => stringOf(recordOf(part.input).query) ?? part.subtitle,
1131
+ summary: (n) => plural(n, "searched the web", (n) => `searched the web ${n} times`),
1132
+ Detail: function WebSearchDetail({ part }) {
1133
+ const results = Array.isArray(part.result) ? part.result : recordOf(part.result).results;
1134
+ if (part.error) return /* @__PURE__ */ jsx("p", {
1135
+ className: "text-sm text-chat-danger",
1136
+ children: part.error
1137
+ });
1138
+ if (!results?.length) return /* @__PURE__ */ jsx("p", {
1139
+ className: "text-sm text-chat-muted",
1140
+ children: "No results"
1141
+ });
1142
+ return /* @__PURE__ */ jsx("ul", {
1143
+ className: "-mx-1",
1144
+ children: results.map((result) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("a", {
1145
+ href: result.url,
1146
+ target: "_blank",
1147
+ rel: "noreferrer",
1148
+ 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",
1149
+ children: [
1150
+ result.favicon ? /* @__PURE__ */ jsx("img", {
1151
+ src: result.favicon,
1152
+ alt: "",
1153
+ className: "size-4 shrink-0 rounded-sm"
1154
+ }) : /* @__PURE__ */ jsx(Globe, {
1155
+ "aria-hidden": true,
1156
+ className: "size-4 shrink-0"
1157
+ }),
1158
+ /* @__PURE__ */ jsx("span", {
1159
+ className: "min-w-0 flex-1 truncate",
1160
+ children: result.title
1161
+ }),
1162
+ /* @__PURE__ */ jsx("span", {
1163
+ className: "shrink-0 text-sm",
1164
+ children: hostOf(result.url)
1165
+ })
1166
+ ]
1167
+ }) }, result.url))
1168
+ });
1169
+ }
1170
+ };
1171
+ /** Memory lookups, e.g. Anthropic's memory tool. Input `{ path }`. */
1172
+ const memoryTool = {
1173
+ label: () => "Recalled memory",
1174
+ subtitle: (part) => {
1175
+ return stringOf(recordOf(part.input).path)?.split("/").at(-1) ?? part.subtitle;
1176
+ },
1177
+ summary: () => "recalled memory"
1178
+ };
1179
+ const defaultToolViews = {
1180
+ bash: commandTool,
1181
+ shell: commandTool,
1182
+ web_search: webSearchTool,
1183
+ memory: memoryTool,
1184
+ memory_view: memoryTool
1185
+ };
1186
+ function hostOf(url) {
1187
+ try {
1188
+ return new URL(url).host;
1189
+ } catch {
1190
+ return url;
1191
+ }
1192
+ }
1193
+ /** "9s", "1m 5s". */
1194
+ function formatDuration(ms) {
1195
+ const seconds = Math.max(1, Math.round(ms / 1e3));
1196
+ return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
1197
+ }
1198
+ // src/components/token-counter.tsx
1199
+ /** Compact token count: 526, 1.4k, 33k, 76.8k, 1M. */
1200
+ function formatTokens(n) {
1201
+ if (n < 1e3) return String(Math.round(n));
1202
+ if (n < 1e6) return `${trim(n / 1e3)}k`;
1203
+ return `${trim(n / 1e6)}M`;
1204
+ }
1205
+ function trim(value) {
1206
+ return (Math.round(value * 10) / 10).toString();
1207
+ }
1208
+ /**
1209
+ * Follows `target` with a number that counts up smoothly instead of jumping. Each frame closes part of the
1210
+ * gap, so bursts of tokens roll in rather than land at once. Jumps straight there with reduced motion.
1211
+ */
1212
+ function useCountUp(target) {
1213
+ const [value, setValue] = useState(target);
1214
+ const valueRef = useRef(target);
1215
+ useEffect(() => {
1216
+ if (typeof window === "undefined" || window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
1217
+ valueRef.current = target;
1218
+ setValue(target);
1219
+ return;
1220
+ }
1221
+ let frame = 0;
1222
+ let last = performance.now();
1223
+ const step = (now) => {
1224
+ const dt = Math.min(Math.max(now - last, 0), 100);
1225
+ last = now;
1226
+ const gap = target - valueRef.current;
1227
+ const move = Math.min(Math.max(Math.abs(gap) * (1 - Math.pow(.1, dt / 250)), 1), Math.abs(gap));
1228
+ const next = Math.abs(gap) < 1 ? target : valueRef.current + Math.sign(gap) * move;
1229
+ valueRef.current = next;
1230
+ setValue(next);
1231
+ if (next !== target) frame = requestAnimationFrame(step);
1232
+ };
1233
+ frame = requestAnimationFrame(step);
1234
+ return () => cancelAnimationFrame(frame);
1235
+ }, [target]);
1236
+ return Math.round(value);
1237
+ }
1238
+ /** A token count that counts up as `tokens` grows and holds still when it stops growing. */
1239
+ function TokenCounter({ tokens, variant = "standalone", className }) {
1240
+ const shown = useCountUp(tokens);
1241
+ if (variant === "inline") return /* @__PURE__ */ jsxs("span", {
1242
+ className: clsx("text-chat-subtle tabular-nums", className),
1243
+ children: [
1244
+ "(",
1245
+ formatTokens(shown),
1246
+ " tokens)"
1247
+ ]
1248
+ });
1249
+ return /* @__PURE__ */ jsxs("span", {
1250
+ className: clsx("inline-flex items-center gap-1 font-chat-sans text-xs text-chat-subtle tabular-nums", className),
1251
+ children: [
1252
+ /* @__PURE__ */ jsx(ArrowDown, {
1253
+ "aria-hidden": true,
1254
+ className: "size-3"
1255
+ }),
1256
+ formatTokens(shown),
1257
+ " tokens"
1258
+ ]
1259
+ });
1260
+ }
1261
+ // src/components/activity-group.tsx
1262
+ /**
1263
+ * The "Ran a command, searched the web ▾" dropdown: a one-line heading that opens a bordered box with one
1264
+ * row per step. Each row expands on its own; tool rows render through the registered `ToolView`.
1265
+ * A group of one step skips the rows: the heading is the step's own label and the box holds its detail.
1266
+ */
1267
+ function ActivityGroup({ parts, defaultExpanded = false, className }) {
1268
+ const views = useToolViews();
1269
+ const [expanded, setExpanded] = useState(defaultExpanded);
1270
+ const live = parts.some(isWorking);
1271
+ const tokens = activityTokens(parts);
1272
+ const after = live && tokens > 0 && /* @__PURE__ */ jsx(TokenCounter, {
1273
+ variant: "inline",
1274
+ tokens,
1275
+ className: "ml-1.5"
1276
+ });
1277
+ return /* @__PURE__ */ jsx("div", {
1278
+ className,
1279
+ children: parts.length === 1 ? /* @__PURE__ */ jsx(SingleStep, {
1280
+ part: parts[0],
1281
+ live,
1282
+ after,
1283
+ expanded,
1284
+ onExpandedChange: setExpanded
1285
+ }) : /* @__PURE__ */ jsx(Disclosure, {
1286
+ isExpanded: expanded,
1287
+ onExpandedChange: setExpanded,
1288
+ summary: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Live, {
1289
+ live,
1290
+ children: activityHeading(parts, views)
1291
+ }), after] }),
1292
+ summaryClassName: "text-[0.9375rem]",
1293
+ panelClassName: "pt-3",
1294
+ children: /* @__PURE__ */ jsx("div", {
1295
+ className: "divide-y divide-chat-divider rounded-chat-md border border-chat-divider",
1296
+ children: parts.map((part) => /* @__PURE__ */ jsx(Step, { part }, part.id))
1297
+ })
1298
+ })
1299
+ });
1300
+ }
1301
+ const SHIMMER_MS = 2e3;
1302
+ /**
1303
+ * Sweeps a highlight across a label while its step is running. The animation starts at the phase of a
1304
+ * shared clock, so every live label sweeps in step and a remount does not restart it.
1305
+ */
1306
+ function Live({ live, children }) {
1307
+ return live ? /* @__PURE__ */ jsx(Shimmer, { children }) : /* @__PURE__ */ jsx(Fragment, { children });
1308
+ }
1309
+ function Shimmer({ children }) {
1310
+ const [delay] = useState(() => `-${Math.round(performance.now() % SHIMMER_MS)}ms`);
1311
+ return /* @__PURE__ */ jsx("span", {
1312
+ className: "chat-shimmer",
1313
+ style: {
1314
+ animationDelay: delay,
1315
+ ["--chat-shimmer-duration"]: `${SHIMMER_MS}ms`
1316
+ },
1317
+ children
1318
+ });
1319
+ }
1320
+ function SingleStep({ part, live, after, expanded, onExpandedChange }) {
1321
+ const { summary, detail } = useStepContent(part);
1322
+ return /* @__PURE__ */ jsx(Disclosure, {
1323
+ isExpanded: expanded,
1324
+ onExpandedChange,
1325
+ summary: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Live, {
1326
+ live,
1327
+ children: summary
1328
+ }), after] }),
1329
+ summaryClassName: "text-[0.9375rem]",
1330
+ panelClassName: "pt-3",
1331
+ children: /* @__PURE__ */ jsx("div", {
1332
+ className: "rounded-chat-md border border-chat-divider px-3 py-2",
1333
+ children: detail
1334
+ })
1335
+ });
1336
+ }
1337
+ function Step({ part }) {
1338
+ const { summary, detail } = useStepContent(part);
1339
+ const live = isWorking(part);
1340
+ return /* @__PURE__ */ jsx(Disclosure, {
1341
+ className: "px-3 py-2",
1342
+ summaryClassName: "text-[0.9375rem]",
1343
+ panelClassName: "pt-2 pb-1",
1344
+ summary: /* @__PURE__ */ jsx(Live, {
1345
+ live,
1346
+ children: summary
1347
+ }),
1348
+ children: detail
1349
+ });
1350
+ }
1351
+ /** The label line and expanded body of one step. Tool calls go through their registered view. */
1352
+ function useStepContent(part) {
1353
+ const view = useToolView(part.type === "tool_call" ? part.name : "");
1354
+ if (part.type === "thinking") return {
1355
+ summary: activityLabel(part),
1356
+ detail: part.visibility === "hidden" || !part.text ? /* @__PURE__ */ jsx("p", {
1357
+ className: "text-sm text-chat-subtle",
1358
+ children: "The model's reasoning is not shown."
1359
+ }) : /* @__PURE__ */ jsx("div", {
1360
+ className: "max-h-96 overflow-y-auto [scrollbar-width:thin]",
1361
+ children: /* @__PURE__ */ jsx(Markdown, {
1362
+ className: "chat-thinking",
1363
+ streaming: part.status === "streaming",
1364
+ children: part.text
1365
+ })
1366
+ })
1367
+ };
1368
+ const subtitle = view.subtitle ? view.subtitle(part) : part.subtitle;
1369
+ const Detail = view.Detail ?? DefaultDetail;
1370
+ return {
1371
+ summary: /* @__PURE__ */ jsxs("span", {
1372
+ title: view.hint?.(part),
1373
+ children: [activityLabel(part, view), subtitle && /* @__PURE__ */ jsx("span", {
1374
+ className: "ml-1.5 text-chat-fg",
1375
+ children: subtitle
1376
+ })]
1377
+ }),
1378
+ detail: /* @__PURE__ */ jsx(Detail, { part })
1379
+ };
1380
+ }
1381
+ function DefaultDetail({ part }) {
1382
+ if (part.error) return /* @__PURE__ */ jsx("p", {
1383
+ className: "text-sm text-chat-danger",
1384
+ children: part.error
1385
+ });
1386
+ const body = part.result ?? part.input ?? part.inputJson;
1387
+ if (typeof body === "string") return /* @__PURE__ */ jsx("p", {
1388
+ className: "text-[0.9375rem] leading-relaxed whitespace-pre-wrap text-chat-muted",
1389
+ children: body
1390
+ });
1391
+ return /* @__PURE__ */ jsx("pre", {
1392
+ className: "max-h-56 overflow-auto font-chat-mono text-[0.8125rem] leading-relaxed text-chat-muted",
1393
+ children: JSON.stringify(body, null, 2)
1394
+ });
1395
+ }
1396
+ // src/components/attachments.tsx
1397
+ let counter = 0;
1398
+ const draftId = () => `att_${Date.now().toString(36)}_${(counter++).toString(36)}`;
1399
+ /** Composer attachments: pasted text, images with thumbnails, files, each uploading on its own. */
1400
+ function useAttachments({ upload } = {}) {
1401
+ const [attachments, setAttachments] = useState([]);
1402
+ const uploads = useRef(/* @__PURE__ */ new Map());
1403
+ const uploadRef = useRef(upload);
1404
+ uploadRef.current = upload;
1405
+ const patch = useCallback((id, fields) => {
1406
+ setAttachments((list) => list.map((a) => a.id === id ? {
1407
+ ...a,
1408
+ ...fields
1409
+ } : a));
1410
+ }, []);
1411
+ const addFiles = useCallback((files) => {
1412
+ for (const file of files) {
1413
+ const id = draftId();
1414
+ const image = file.type.startsWith("image/");
1415
+ const doUpload = uploadRef.current;
1416
+ const draft = {
1417
+ id,
1418
+ kind: image ? "image" : "file",
1419
+ name: file.name,
1420
+ mime: file.type || "application/octet-stream",
1421
+ size: file.size,
1422
+ status: doUpload ? "uploading" : "ready",
1423
+ ...image && { previewUrl: URL.createObjectURL(file) }
1424
+ };
1425
+ setAttachments((list) => [...list, draft]);
1426
+ if (!doUpload) continue;
1427
+ const controller = new AbortController();
1428
+ uploads.current.set(id, controller);
1429
+ doUpload(file, controller.signal).then((result) => patch(id, {
1430
+ status: "ready",
1431
+ fileId: result.fileId,
1432
+ ...result.url && { url: result.url }
1433
+ })).catch((error) => {
1434
+ if (!controller.signal.aborted) patch(id, {
1435
+ status: "error",
1436
+ error: error instanceof Error ? error.message : "Upload failed"
1437
+ });
1438
+ }).finally(() => uploads.current.delete(id));
1439
+ }
1440
+ }, [patch]);
1441
+ const addText = useCallback((text) => {
1442
+ setAttachments((list) => [...list, {
1443
+ id: draftId(),
1444
+ kind: "text",
1445
+ name: "Pasted text",
1446
+ mime: "text/plain",
1447
+ size: new Blob([text]).size,
1448
+ text,
1449
+ status: "ready"
1450
+ }]);
1451
+ }, []);
1452
+ const updateText = useCallback((id, text) => patch(id, {
1453
+ text,
1454
+ size: new Blob([text]).size
1455
+ }), [patch]);
1456
+ const remove = useCallback((id) => {
1457
+ uploads.current.get(id)?.abort();
1458
+ setAttachments((list) => {
1459
+ const gone = list.find((a) => a.id === id);
1460
+ if (gone?.previewUrl) URL.revokeObjectURL(gone.previewUrl);
1461
+ return list.filter((a) => a.id !== id);
1462
+ });
1463
+ }, []);
1464
+ const clear = useCallback(() => setAttachments([]), []);
1465
+ const toParts = useCallback(() => attachments.filter((a) => a.status === "ready").map((a) => ({
1466
+ type: "attachment",
1467
+ id: a.id,
1468
+ status: "done",
1469
+ fileId: a.fileId ?? a.id,
1470
+ name: a.name,
1471
+ mime: a.mime,
1472
+ size: a.size,
1473
+ ...(a.url ?? a.previewUrl) && { url: a.url ?? a.previewUrl },
1474
+ ...a.text !== void 0 && { text: a.text }
1475
+ })), [attachments]);
1476
+ useEffect(() => () => uploads.current.forEach((c) => c.abort()), []);
1477
+ return {
1478
+ attachments,
1479
+ uploading: attachments.some((a) => a.status === "uploading"),
1480
+ addFiles,
1481
+ addText,
1482
+ updateText,
1483
+ remove,
1484
+ clear,
1485
+ toParts
1486
+ };
1487
+ }
1488
+ const extension = (name) => /\.([a-z0-9]{1,5})$/i.exec(name)?.[1]?.toUpperCase();
1489
+ /** A square card: pasted text preview with a PASTED badge, an image thumbnail, or a file with its type. */
1490
+ function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", className }) {
1491
+ const image = a.mime.startsWith("image/") && a.url;
1492
+ const pasted = a.text !== void 0;
1493
+ const uploading = a.status === "uploading";
1494
+ const failed = a.status === "error";
1495
+ return /* @__PURE__ */ jsxs("div", {
1496
+ 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),
1497
+ title: failed ? a.error ?? "Upload failed" : a.name,
1498
+ children: [
1499
+ /* @__PURE__ */ jsx("div", {
1500
+ className: clsx("absolute inset-0 overflow-hidden rounded-[inherit]", uploading && "chat-shimmer-surface"),
1501
+ children: image ? /* @__PURE__ */ jsx("img", {
1502
+ src: a.url,
1503
+ alt: a.name,
1504
+ className: clsx("size-full object-cover", uploading && "opacity-60")
1505
+ }) : pasted ? /* @__PURE__ */ jsx("p", {
1506
+ className: "h-full overflow-hidden p-2.5 pb-9 text-[0.5625rem] leading-snug break-all text-chat-muted [mask-image:linear-gradient(to_bottom,black_45%,transparent_72%)]",
1507
+ children: a.text
1508
+ }) : /* @__PURE__ */ jsxs("div", {
1509
+ className: "flex h-full flex-col gap-1.5 p-2.5",
1510
+ children: [/* @__PURE__ */ jsx(FileText, {
1511
+ "aria-hidden": true,
1512
+ className: "size-5 shrink-0 text-chat-muted"
1513
+ }), /* @__PURE__ */ jsx("span", {
1514
+ className: "line-clamp-3 text-xs break-words text-chat-fg",
1515
+ children: a.name
1516
+ })]
1517
+ })
1518
+ }),
1519
+ !image && /* @__PURE__ */ jsxs("div", {
1520
+ className: "absolute inset-x-2 bottom-2 flex items-end justify-between gap-1",
1521
+ children: [/* @__PURE__ */ jsx("span", {
1522
+ 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"),
1523
+ children: failed ? "Failed" : pasted ? "Pasted" : extension(a.name) ?? "File"
1524
+ }), pasted && onOpen && /* @__PURE__ */ jsx(IconButton, {
1525
+ label: "Edit pasted text",
1526
+ size: "sm",
1527
+ onPress: onOpen,
1528
+ className: "size-6 border border-chat-border-strong bg-chat-bg [&_svg]:size-3.5 [@media(pointer:coarse)]:size-7!",
1529
+ children: /* @__PURE__ */ jsx(Pencil, {})
1530
+ })]
1531
+ }),
1532
+ pasted && onOpen && !onRemove && /* @__PURE__ */ jsx("button", {
1533
+ type: "button",
1534
+ "aria-label": "View pasted text",
1535
+ onClick: onOpen,
1536
+ className: "absolute inset-0 cursor-pointer rounded-[inherit] outline-none focus-visible:ring-2 focus-visible:ring-chat-focus"
1537
+ }),
1538
+ onRemove && /* @__PURE__ */ jsx(IconButton, {
1539
+ label: `Remove ${a.name}`,
1540
+ variant: "subtle",
1541
+ size: "sm",
1542
+ tooltip: false,
1543
+ onPress: onRemove,
1544
+ 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"),
1545
+ children: /* @__PURE__ */ jsx(X, {})
1546
+ })
1547
+ ]
1548
+ });
1549
+ }
1550
+ /** The composer's row of cards above the text. Renders nothing when empty. */
1551
+ function AttachmentRow({ attachments, onRemove, onEditText }) {
1552
+ const [editing, setEditing] = useState();
1553
+ if (attachments.length === 0) return null;
1554
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("ul", {
1555
+ "aria-label": "Attachments",
1556
+ className: "flex gap-2.5 overflow-x-auto px-0.5 pt-2.5 pb-1 [scrollbar-width:thin]",
1557
+ children: attachments.map((a) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(AttachmentCard, {
1558
+ attachment: {
1559
+ ...a,
1560
+ ...a.previewUrl && { url: a.previewUrl }
1561
+ },
1562
+ onRemove: () => onRemove(a.id),
1563
+ ...a.kind === "text" && { onOpen: () => setEditing(a) }
1564
+ }) }, a.id))
1565
+ }), /* @__PURE__ */ jsx(PastedTextDialog, {
1566
+ text: editing?.text,
1567
+ onClose: () => setEditing(void 0),
1568
+ ...onEditText && editing && { onSave: (text) => onEditText(editing.id, text) }
1569
+ })] });
1570
+ }
1571
+ /** Shows pasted text in a modal, editable when `onSave` is set. */
1572
+ function PastedTextDialog({ text, onClose, onSave, title = "Pasted text" }) {
1573
+ const [draft, setDraft] = useState(text ?? "");
1574
+ useEffect(() => setDraft(text ?? ""), [text]);
1575
+ return /* @__PURE__ */ jsxs(Modal, {
1576
+ isOpen: text !== void 0,
1577
+ onOpenChange: (open) => !open && onClose(),
1578
+ title,
1579
+ children: [/* @__PURE__ */ jsx("textarea", {
1580
+ value: draft,
1581
+ onChange: (e) => setDraft(e.target.value),
1582
+ readOnly: !onSave,
1583
+ "aria-label": title,
1584
+ 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]"
1585
+ }), /* @__PURE__ */ jsxs("div", {
1586
+ className: "flex justify-end gap-2 px-5 py-4",
1587
+ children: [/* @__PURE__ */ jsx(Button, {
1588
+ variant: "subtle",
1589
+ onPress: onClose,
1590
+ children: onSave ? "Cancel" : "Close"
1591
+ }), onSave && /* @__PURE__ */ jsx(Button, {
1592
+ variant: "primary",
1593
+ onPress: () => {
1594
+ onSave(draft);
1595
+ onClose();
1596
+ },
1597
+ children: "Save"
1598
+ })]
1599
+ })]
1600
+ });
1601
+ }
1602
+ // src/components/messages.tsx
1603
+ /** "just now", "5 minutes ago", "21 hours ago", "yesterday", then a date. */
1604
+ function formatRelativeTime(iso, now = Date.now()) {
1605
+ const seconds = (new Date(iso).getTime() - now) / 1e3;
1606
+ const rtf = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
1607
+ if (Math.abs(seconds) < 60) return "just now";
1608
+ if (Math.abs(seconds) < 3600) return rtf.format(Math.round(seconds / 60), "minute");
1609
+ if (Math.abs(seconds) < 86400) return rtf.format(Math.round(seconds / 3600), "hour");
1610
+ if (Math.abs(seconds) < 604800) return rtf.format(Math.round(seconds / 86400), "day");
1611
+ return new Date(iso).toLocaleDateString(void 0, {
1612
+ month: "short",
1613
+ day: "numeric"
1614
+ });
1615
+ }
1616
+ /** Plain text of a message, for copy and read aloud. */
1617
+ function messageText(message) {
1618
+ return message.parts.map((p) => p.type === "text" ? p.text : p.type === "custom" ? p.text ?? "" : "").filter(Boolean).join("\n\n");
1619
+ }
1620
+ function CopyButton({ text }) {
1621
+ const [copied, setCopied] = useState(false);
1622
+ return /* @__PURE__ */ jsx(IconButton, {
1623
+ label: copied ? "Copied" : "Copy",
1624
+ size: "sm",
1625
+ onPress: () => {
1626
+ navigator.clipboard?.writeText(text);
1627
+ setCopied(true);
1628
+ setTimeout(() => setCopied(false), 1500);
1629
+ },
1630
+ children: copied ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {})
1631
+ });
1632
+ }
1633
+ const timeClass = "px-1 font-chat-sans text-xs text-chat-subtle";
1634
+ /** The user's turn: a bubble on the right, with time, retry, edit and copy under it on hover. */
1635
+ function UserMessage({ message, onRetry, onEdit, actions, className }) {
1636
+ const text = messageText(message);
1637
+ const attachments = message.parts.filter((p) => p.type === "attachment");
1638
+ const [viewing, setViewing] = useState();
1639
+ return /* @__PURE__ */ jsxs("div", {
1640
+ className: clsx("group/message flex flex-col items-end", className),
1641
+ children: [
1642
+ attachments.length > 0 && /* @__PURE__ */ jsx("ul", {
1643
+ "aria-label": "Attachments",
1644
+ className: "mb-2 flex max-w-[85%] flex-wrap justify-end gap-2",
1645
+ children: attachments.map((a) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(AttachmentCard, {
1646
+ attachment: a,
1647
+ size: "sm",
1648
+ ...a.text !== void 0 && { onOpen: () => setViewing(a.text) }
1649
+ }) }, a.id))
1650
+ }),
1651
+ /* @__PURE__ */ jsx(PastedTextDialog, {
1652
+ text: viewing,
1653
+ onClose: () => setViewing(void 0)
1654
+ }),
1655
+ text && /* @__PURE__ */ jsx("div", {
1656
+ 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",
1657
+ children: text
1658
+ }),
1659
+ /* @__PURE__ */ jsxs("div", {
1660
+ 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",
1661
+ children: [
1662
+ /* @__PURE__ */ jsx("span", {
1663
+ className: timeClass,
1664
+ children: formatRelativeTime(message.createdAt)
1665
+ }),
1666
+ onRetry && /* @__PURE__ */ jsx(IconButton, {
1667
+ label: "Retry",
1668
+ size: "sm",
1669
+ onPress: onRetry,
1670
+ children: /* @__PURE__ */ jsx(RefreshCw, {})
1671
+ }),
1672
+ onEdit && /* @__PURE__ */ jsx(IconButton, {
1673
+ label: "Edit",
1674
+ size: "sm",
1675
+ onPress: onEdit,
1676
+ children: /* @__PURE__ */ jsx(Pencil, {})
1677
+ }),
1678
+ /* @__PURE__ */ jsx(CopyButton, { text }),
1679
+ actions
1680
+ ]
1681
+ })
1682
+ ]
1683
+ });
1684
+ }
1685
+ /**
1686
+ * The assistant's turn: activity groups (each with its own token count), markdown, and other parts in
1687
+ * order. When done, copy / read aloud / feedback / retry and the time sit under it.
1688
+ */
1689
+ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPart, actions, className }) {
1690
+ const streaming = message.status === "streaming";
1691
+ const views = useToolViews();
1692
+ const blocks = toBlocks(message.parts, (part) => views[part.name]?.Block !== void 0);
1693
+ return /* @__PURE__ */ jsxs("div", {
1694
+ className: clsx("group/message flex flex-col gap-4", className),
1695
+ children: [
1696
+ blocks.map((block) => {
1697
+ if (block.kind === "activity") return /* @__PURE__ */ jsx(ActivityGroup, { parts: block.parts }, block.id);
1698
+ if (block.kind === "tool") {
1699
+ const ToolBlock = views[block.part.name].Block;
1700
+ return /* @__PURE__ */ jsx(ToolBlock, { part: block.part }, block.id);
1701
+ }
1702
+ const part = block.part;
1703
+ if (part.type === "text") return /* @__PURE__ */ jsx(Markdown, {
1704
+ streaming: part.status === "streaming",
1705
+ children: part.text
1706
+ }, part.id);
1707
+ const custom = renderPart?.(part);
1708
+ if (custom) return /* @__PURE__ */ jsx("div", { children: custom }, part.id);
1709
+ if (part.type === "custom" && part.text) return /* @__PURE__ */ jsx(Markdown, { children: part.text }, part.id);
1710
+ return null;
1711
+ }),
1712
+ message.status === "error" && /* @__PURE__ */ jsx("p", {
1713
+ className: "font-chat-sans text-sm text-chat-danger",
1714
+ children: message.error ?? "Something went wrong."
1715
+ }),
1716
+ streaming ? /* @__PURE__ */ jsx("div", { className: "h-7" }) : /* @__PURE__ */ jsxs("div", {
1717
+ className: "-ml-1.5 flex h-7 items-center gap-0.5",
1718
+ children: [
1719
+ /* @__PURE__ */ jsx(CopyButton, { text: messageText(message) }),
1720
+ onReadAloud && /* @__PURE__ */ jsx(IconButton, {
1721
+ label: "Read aloud",
1722
+ size: "sm",
1723
+ onPress: onReadAloud,
1724
+ children: /* @__PURE__ */ jsx(Volume2, {})
1725
+ }),
1726
+ onFeedback && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(IconButton, {
1727
+ label: "Good response",
1728
+ size: "sm",
1729
+ onPress: () => onFeedback("good"),
1730
+ children: /* @__PURE__ */ jsx(ThumbsUp, {})
1731
+ }), /* @__PURE__ */ jsx(IconButton, {
1732
+ label: "Bad response",
1733
+ size: "sm",
1734
+ onPress: () => onFeedback("bad"),
1735
+ children: /* @__PURE__ */ jsx(ThumbsDown, {})
1736
+ })] }),
1737
+ onRetry && /* @__PURE__ */ jsx(IconButton, {
1738
+ label: "Retry",
1739
+ size: "sm",
1740
+ onPress: onRetry,
1741
+ children: /* @__PURE__ */ jsx(RefreshCw, {})
1742
+ }),
1743
+ actions,
1744
+ /* @__PURE__ */ jsx("span", {
1745
+ className: timeClass,
1746
+ children: formatRelativeTime(message.createdAt)
1747
+ })
1748
+ ]
1749
+ })
1750
+ ]
1751
+ });
1752
+ }
1753
+ // src/components/approval-card.tsx
1754
+ /**
1755
+ * Asks the user before a tool runs, above the composer. Shows the tool's label and what it will do: its
1756
+ * view's `Detail` when it has one, else the input. Replying in the composer instead declines it.
1757
+ */
1758
+ function ApprovalCard({ approval, onRespond, className }) {
1759
+ const { part } = approval;
1760
+ const view = useToolView(part.name);
1761
+ const Detail = view.Detail;
1762
+ const preview = {
1763
+ ...part,
1764
+ status: "awaiting"
1765
+ };
1766
+ return /* @__PURE__ */ jsxs("div", {
1767
+ role: "alertdialog",
1768
+ "aria-label": `Allow ${part.name}?`,
1769
+ 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),
1770
+ children: [
1771
+ /* @__PURE__ */ jsxs("div", {
1772
+ className: "flex items-center gap-2 text-sm",
1773
+ children: [/* @__PURE__ */ jsx(ShieldQuestion, {
1774
+ "aria-hidden": true,
1775
+ className: "size-4 shrink-0 text-chat-accent"
1776
+ }), /* @__PURE__ */ jsx("span", {
1777
+ className: "min-w-0 flex-1 font-medium",
1778
+ children: view.ask?.(part) ?? /* @__PURE__ */ jsxs(Fragment, { children: [
1779
+ "Allow ",
1780
+ activityLabel(part, view),
1781
+ "?"
1782
+ ] })
1783
+ })]
1784
+ }),
1785
+ /* @__PURE__ */ jsx("div", {
1786
+ className: "max-h-48 overflow-auto [scrollbar-width:thin]",
1787
+ children: Detail ? /* @__PURE__ */ jsx(Detail, { part: preview }) : /* @__PURE__ */ jsx(CodeBlock, {
1788
+ variant: "boxed",
1789
+ label: part.name,
1790
+ code: JSON.stringify(part.input ?? {}, null, 2)
1791
+ })
1792
+ }),
1793
+ /* @__PURE__ */ jsxs("div", {
1794
+ className: "flex justify-end gap-2",
1795
+ children: [/* @__PURE__ */ jsx(Button, {
1796
+ variant: "ghost",
1797
+ className: "border border-chat-border-strong",
1798
+ onPress: () => onRespond(part.id, false),
1799
+ children: "Deny"
1800
+ }), /* @__PURE__ */ jsx(Button, {
1801
+ variant: "primary",
1802
+ autoFocus: true,
1803
+ onPress: () => onRespond(part.id, true),
1804
+ children: "Allow"
1805
+ })]
1806
+ })
1807
+ ]
1808
+ });
1809
+ }
1810
+ // src/components/context-panel.tsx
1811
+ const FREE_COLOR = "var(--chat-raised)";
1812
+ const DEFAULT_COLOR = "var(--chat-subtle)";
1813
+ /**
1814
+ * What fills the context window: a usage line, a segmented bar, one row per category with its share, and
1815
+ * expandable lists (each MCP tool, each memory file). Deferred categories are listed but not counted.
1816
+ */
1817
+ function ContextPanel({ usage, title = "Context", defaultExpanded = true, className }) {
1818
+ const counted = usage.categories.filter((c) => !c.deferred);
1819
+ const deferred = usage.categories.filter((c) => c.deferred);
1820
+ const used = counted.reduce((sum, c) => sum + (c.reserved ? 0 : c.tokens), 0);
1821
+ const reserved = counted.reduce((sum, c) => sum + (c.reserved ? c.tokens : 0), 0);
1822
+ const free = Math.max(usage.limit - used - reserved, 0);
1823
+ const share = (tokens) => formatShare(tokens / usage.limit);
1824
+ return /* @__PURE__ */ jsxs(Disclosure, {
1825
+ summary: title,
1826
+ defaultExpanded,
1827
+ className: clsx("font-chat-sans text-chat-fg", className),
1828
+ summaryClassName: "text-[0.9375rem] text-chat-fg",
1829
+ panelClassName: "pt-3",
1830
+ children: [
1831
+ /* @__PURE__ */ jsxs("div", {
1832
+ className: "mb-2 flex items-baseline justify-between text-[0.9375rem]",
1833
+ children: [/* @__PURE__ */ jsxs("span", { children: [Math.round(used / usage.limit * 100), "% used"] }), /* @__PURE__ */ jsxs("span", {
1834
+ className: "text-sm text-chat-muted tabular-nums",
1835
+ children: [
1836
+ formatTokens(used),
1837
+ " / ",
1838
+ formatTokens(usage.limit)
1839
+ ]
1840
+ })]
1841
+ }),
1842
+ /* @__PURE__ */ jsx("div", {
1843
+ role: "meter",
1844
+ "aria-label": "Context used",
1845
+ "aria-valuemin": 0,
1846
+ "aria-valuemax": usage.limit,
1847
+ "aria-valuenow": used,
1848
+ className: "mb-3 flex h-1 gap-px overflow-hidden rounded-full",
1849
+ style: { background: FREE_COLOR },
1850
+ children: counted.map((c) => /* @__PURE__ */ jsx("span", {
1851
+ className: "h-full min-w-[2px]",
1852
+ style: {
1853
+ width: `${c.tokens / usage.limit * 100}%`,
1854
+ background: c.color ?? DEFAULT_COLOR
1855
+ }
1856
+ }, c.id))
1857
+ }),
1858
+ /* @__PURE__ */ jsxs("ul", {
1859
+ className: "space-y-1",
1860
+ children: [counted.map((c) => /* @__PURE__ */ jsx(CategoryRow, {
1861
+ category: c,
1862
+ share: share(c.tokens)
1863
+ }, c.id)), /* @__PURE__ */ jsx(CategoryRow, {
1864
+ category: {
1865
+ id: "free",
1866
+ label: "Free space",
1867
+ tokens: free,
1868
+ color: FREE_COLOR
1869
+ },
1870
+ share: share(free)
1871
+ })]
1872
+ }),
1873
+ deferred.length > 0 && /* @__PURE__ */ jsx("ul", {
1874
+ className: "mt-3 space-y-1",
1875
+ children: deferred.map((c) => /* @__PURE__ */ jsx(CategoryRow, {
1876
+ category: c,
1877
+ share: "—"
1878
+ }, c.id))
1879
+ }),
1880
+ usage.groups && usage.groups.length > 0 && /* @__PURE__ */ jsx("div", {
1881
+ className: "mt-3 space-y-1.5",
1882
+ children: usage.groups.map((g) => /* @__PURE__ */ jsx(GroupList, { group: g }, g.id))
1883
+ })
1884
+ ]
1885
+ });
1886
+ }
1887
+ function CategoryRow({ category, share }) {
1888
+ const label = category.hint ? /* @__PURE__ */ jsx(Tooltip, {
1889
+ content: category.hint,
1890
+ children: /* @__PURE__ */ jsx(Focusable, { children: /* @__PURE__ */ jsx("span", {
1891
+ tabIndex: 0,
1892
+ className: "cursor-help underline decoration-chat-subtle decoration-dotted underline-offset-4 outline-none",
1893
+ children: category.label
1894
+ }) })
1895
+ }) : category.label;
1896
+ return /* @__PURE__ */ jsxs("li", {
1897
+ className: "flex items-center gap-2.5 text-sm",
1898
+ children: [
1899
+ /* @__PURE__ */ jsx("span", {
1900
+ "aria-hidden": true,
1901
+ className: "size-2.5 shrink-0 rounded-[2px]",
1902
+ style: { background: category.color ?? (category.deferred ? "var(--chat-selected)" : DEFAULT_COLOR) }
1903
+ }),
1904
+ /* @__PURE__ */ jsx("span", {
1905
+ className: "min-w-0 flex-1 truncate text-chat-muted",
1906
+ children: label
1907
+ }),
1908
+ /* @__PURE__ */ jsx("span", {
1909
+ className: "w-14 text-right text-chat-muted tabular-nums",
1910
+ children: formatTokens(category.tokens)
1911
+ }),
1912
+ /* @__PURE__ */ jsx("span", {
1913
+ className: "w-12 text-right text-chat-muted tabular-nums",
1914
+ children: share
1915
+ })
1916
+ ]
1917
+ });
1918
+ }
1919
+ function GroupList({ group }) {
1920
+ return /* @__PURE__ */ jsx(Disclosure, {
1921
+ summary: group.label,
1922
+ trailing: /* @__PURE__ */ jsx("span", {
1923
+ className: "text-sm text-chat-subtle tabular-nums",
1924
+ children: group.items.length
1925
+ }),
1926
+ indicator: "chevron",
1927
+ summaryClassName: "text-sm",
1928
+ panelClassName: "pt-1.5",
1929
+ children: /* @__PURE__ */ jsx("ul", {
1930
+ className: "max-h-72 space-y-1 overflow-y-auto pl-6 [scrollbar-color:var(--chat-border-strong)_transparent] [scrollbar-width:thin]",
1931
+ children: group.items.map((item) => /* @__PURE__ */ jsxs("li", {
1932
+ className: "flex items-center gap-2 text-sm text-chat-muted",
1933
+ children: [/* @__PURE__ */ jsx("span", {
1934
+ className: "min-w-0 flex-1 truncate",
1935
+ children: item.label
1936
+ }), /* @__PURE__ */ jsx("span", {
1937
+ className: "w-12 shrink-0 text-right tabular-nums",
1938
+ children: formatTokens(item.tokens)
1939
+ })]
1940
+ }, item.id))
1941
+ })
1942
+ });
1943
+ }
1944
+ function formatShare(fraction) {
1945
+ return `${(fraction * 100).toFixed(1)}%`;
1946
+ }
1947
+ // src/components/composer.tsx
1948
+ /** Touch keyboards have no easy Shift+Enter, so there Enter adds a line and the button sends. */
1949
+ const coarsePointer = () => typeof window !== "undefined" && window.matchMedia?.("(pointer: coarse)").matches;
1950
+ /** The chat box: growing textarea, toolbar slots, send / stop. Enter sends, Shift+Enter adds a line. */
1951
+ 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 }) {
1952
+ const [own, setOwn] = useState("");
1953
+ const text = value ?? own;
1954
+ const setText = (next) => {
1955
+ if (value === void 0) setOwn(next);
1956
+ onChange?.(next);
1957
+ };
1958
+ const area = useRef(null);
1959
+ const fit = useCallback(() => {
1960
+ const el = area.current;
1961
+ if (!el) return;
1962
+ el.style.height = "auto";
1963
+ el.style.height = `${el.scrollHeight}px`;
1964
+ el.style.overflowY = el.scrollHeight > el.clientHeight + 1 ? "auto" : "hidden";
1965
+ }, []);
1966
+ useLayoutEffect(fit, [
1967
+ text,
1968
+ variant,
1969
+ fit
1970
+ ]);
1971
+ useEffect(() => {
1972
+ const el = area.current;
1973
+ if (!el || typeof ResizeObserver === "undefined") return;
1974
+ let width = el.clientWidth;
1975
+ const observer = new ResizeObserver(() => {
1976
+ if (el.clientWidth !== width) width = el.clientWidth, fit();
1977
+ });
1978
+ observer.observe(el);
1979
+ return () => observer.disconnect();
1980
+ }, [fit]);
1981
+ const [dragging, setDragging] = useState(false);
1982
+ const canSend = !busy && !isDisabled && !submitDisabled && (allowEmpty || text.trim().length > 0);
1983
+ const submit = () => {
1984
+ if (!canSend) return;
1985
+ onSubmit(text);
1986
+ setText("");
1987
+ };
1988
+ const onKeyDown = (e) => {
1989
+ if (e.key !== "Enter" || e.shiftKey || e.nativeEvent.isComposing || coarsePointer()) return;
1990
+ e.preventDefault();
1991
+ submit();
1992
+ };
1993
+ const onPaste = (e) => {
1994
+ const files = [...e.clipboardData.files];
1995
+ if (files.length && onFiles) {
1996
+ e.preventDefault();
1997
+ onFiles(files);
1998
+ return;
1999
+ }
2000
+ const pasted = e.clipboardData.getData("text/plain");
2001
+ if (onLongPaste && pasted.length > longPasteChars) {
2002
+ e.preventDefault();
2003
+ onLongPaste(pasted);
2004
+ }
2005
+ };
2006
+ const dropProps = onFiles && {
2007
+ onDragOver: (e) => {
2008
+ if (!e.dataTransfer.types.includes("Files")) return;
2009
+ e.preventDefault();
2010
+ setDragging(true);
2011
+ },
2012
+ onDragLeave: (e) => {
2013
+ if (!e.currentTarget.contains(e.relatedTarget)) setDragging(false);
2014
+ },
2015
+ onDrop: (e) => {
2016
+ if (!e.dataTransfer.files.length) return;
2017
+ e.preventDefault();
2018
+ setDragging(false);
2019
+ onFiles([...e.dataTransfer.files]);
2020
+ }
2021
+ };
2022
+ const compact = variant === "compact";
2023
+ const action = busy ? /* @__PURE__ */ jsx(IconButton, {
2024
+ label: "Stop",
2025
+ variant: "primary",
2026
+ onPress: onStop,
2027
+ className: "rounded-chat-md [&_svg]:size-3.5 [&_svg]:fill-current",
2028
+ children: /* @__PURE__ */ jsx(Square, {})
2029
+ }) : /* @__PURE__ */ jsx(IconButton, {
2030
+ label: "Send",
2031
+ variant: "primary",
2032
+ onPress: submit,
2033
+ isDisabled: !canSend,
2034
+ className: "rounded-chat-md",
2035
+ children: /* @__PURE__ */ jsx(ArrowUp, {})
2036
+ });
2037
+ return /* @__PURE__ */ jsxs("div", {
2038
+ 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),
2039
+ ...dropProps,
2040
+ onPointerDown: (e) => {
2041
+ if (e.target === e.currentTarget) {
2042
+ e.preventDefault();
2043
+ area.current?.focus();
2044
+ }
2045
+ },
2046
+ children: [above, /* @__PURE__ */ jsxs("div", {
2047
+ className: clsx("flex gap-2", compact ? "items-end" : "flex-col"),
2048
+ children: [
2049
+ compact && leading,
2050
+ /* @__PURE__ */ jsx("textarea", {
2051
+ ref: area,
2052
+ rows: 1,
2053
+ value: text,
2054
+ onChange: (e) => setText(e.target.value),
2055
+ onKeyDown,
2056
+ onPaste,
2057
+ placeholder,
2058
+ "aria-label": "Message",
2059
+ disabled: isDisabled,
2060
+ autoFocus,
2061
+ 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")
2062
+ }),
2063
+ compact ? /* @__PURE__ */ jsxs("div", {
2064
+ className: "flex shrink-0 items-center gap-1",
2065
+ children: [trailing, action]
2066
+ }) : /* @__PURE__ */ jsxs("div", {
2067
+ className: "flex items-center gap-1",
2068
+ children: [
2069
+ /* @__PURE__ */ jsx("div", {
2070
+ className: "flex min-w-0 flex-1 items-center gap-1",
2071
+ children: leading
2072
+ }),
2073
+ trailing,
2074
+ action
2075
+ ]
2076
+ })
2077
+ ]
2078
+ })]
2079
+ });
2080
+ }
2081
+ /** The `+` button that opens the attach menu above the composer. */
2082
+ function AttachButton({ children, label = "Add files and more" }) {
2083
+ return /* @__PURE__ */ jsxs(MenuTrigger, { children: [/* @__PURE__ */ jsx(IconButton, {
2084
+ label,
2085
+ children: /* @__PURE__ */ jsx(Plus, {})
2086
+ }), /* @__PURE__ */ jsx(Popover, {
2087
+ placement: "top start",
2088
+ children
2089
+ })] });
2090
+ }
2091
+ // src/components/model-picker.tsx
2092
+ /** "Opus 5.5 High ⌄": the models, an Effort submenu for models that have levels, and More models. */
2093
+ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, effortNote = "Higher effort means more thorough responses, but takes longer and uses your limits faster.", footnote, className }) {
2094
+ const current = models.find((m) => m.id === model);
2095
+ const efforts = current?.efforts ?? [];
2096
+ const currentEffort = efforts.find((e) => e.id === (effort ?? current?.defaultEffort));
2097
+ const main = models.filter((m) => !m.secondary);
2098
+ const more = models.filter((m) => m.secondary);
2099
+ const item = (m) => /* @__PURE__ */ jsx(MenuItem, {
2100
+ id: m.id,
2101
+ description: m.disabledReason ?? m.description,
2102
+ isDisabled: m.disabled ?? false,
2103
+ children: m.label
2104
+ }, m.id);
2105
+ const pick = (keys) => {
2106
+ if (keys === "all") return;
2107
+ const [id] = keys;
2108
+ if (id !== void 0) onModelChange(String(id));
2109
+ };
2110
+ return /* @__PURE__ */ jsxs(MenuTrigger, { children: [/* @__PURE__ */ jsxs(Button, {
2111
+ size: "sm",
2112
+ "aria-label": `Model: ${current?.label ?? model}`,
2113
+ className: clsx("text-chat-muted data-hovered:text-chat-fg", className),
2114
+ children: [
2115
+ /* @__PURE__ */ jsx("span", {
2116
+ className: "min-w-0 truncate text-chat-fg",
2117
+ children: current?.label ?? model
2118
+ }),
2119
+ currentEffort && /* @__PURE__ */ jsx("span", {
2120
+ className: "chat-picker-detail text-chat-muted",
2121
+ children: currentEffort.label
2122
+ }),
2123
+ /* @__PURE__ */ jsx(ChevronDown, {
2124
+ "aria-hidden": true,
2125
+ className: "size-3.5"
2126
+ })
2127
+ ]
2128
+ }), /* @__PURE__ */ jsxs(Popover, {
2129
+ placement: "top end",
2130
+ className: "w-72",
2131
+ children: [/* @__PURE__ */ jsxs(Menu, {
2132
+ "aria-label": "Model",
2133
+ children: [
2134
+ /* @__PURE__ */ jsx(MenuSection, {
2135
+ selectionMode: "single",
2136
+ selectedKeys: [model],
2137
+ onSelectionChange: pick,
2138
+ children: main.map(item)
2139
+ }),
2140
+ (efforts.length > 0 || more.length > 0) && /* @__PURE__ */ jsx(MenuSeparator, {}),
2141
+ efforts.length > 0 && onEffortChange && /* @__PURE__ */ jsxs(SubmenuTrigger, { children: [/* @__PURE__ */ jsx(MenuItem, {
2142
+ id: "__effort",
2143
+ detail: currentEffort?.label,
2144
+ children: "Effort"
2145
+ }), /* @__PURE__ */ jsxs(Popover, {
2146
+ placement: "end top",
2147
+ className: "w-80",
2148
+ children: [/* @__PURE__ */ jsx(MenuNote, { children: effortNote }), /* @__PURE__ */ jsx(Menu, {
2149
+ "aria-label": "Effort",
2150
+ selectionMode: "single",
2151
+ selectedKeys: currentEffort ? [currentEffort.id] : [],
2152
+ onSelectionChange: (keys) => {
2153
+ if (keys === "all") return;
2154
+ const [id] = keys;
2155
+ if (id !== void 0) onEffortChange(String(id));
2156
+ },
2157
+ children: efforts.map((e) => /* @__PURE__ */ jsx(MenuItem, {
2158
+ id: e.id,
2159
+ textValue: e.label,
2160
+ children: /* @__PURE__ */ jsxs("span", {
2161
+ className: "flex items-center gap-2",
2162
+ children: [
2163
+ e.label,
2164
+ e.recommended && /* @__PURE__ */ jsx(Badge, { children: "Recommended" }),
2165
+ e.warning && /* @__PURE__ */ jsx(Badge, {
2166
+ tone: "warning",
2167
+ icon: /* @__PURE__ */ jsx(AlertTriangle, {}),
2168
+ children: e.warning
2169
+ })
2170
+ ]
2171
+ })
2172
+ }, e.id))
2173
+ })]
2174
+ })] }),
2175
+ more.length > 0 && /* @__PURE__ */ jsxs(SubmenuTrigger, { children: [/* @__PURE__ */ jsx(MenuItem, {
2176
+ id: "__more",
2177
+ children: "More models"
2178
+ }), /* @__PURE__ */ jsx(Popover, {
2179
+ placement: "end top",
2180
+ className: "w-72",
2181
+ children: /* @__PURE__ */ jsx(Menu, {
2182
+ "aria-label": "More models",
2183
+ selectionMode: "single",
2184
+ selectedKeys: [model],
2185
+ onSelectionChange: pick,
2186
+ children: more.map(item)
2187
+ })
2188
+ })] })
2189
+ ]
2190
+ }), footnote && /* @__PURE__ */ jsx(MenuNote, {
2191
+ className: "border-t border-chat-popover-border text-xs",
2192
+ children: footnote
2193
+ })]
2194
+ })] });
2195
+ }
2196
+ // src/components/message-list.tsx
2197
+ /** How close to the bottom still counts as "at the bottom", in px. */
2198
+ const STICK_SLOP = 48;
2199
+ /**
2200
+ * The scrolling conversation. It follows a streaming reply while the reader is at the bottom and leaves
2201
+ * them alone once they scroll up, with a button to jump back. Sending a message always jumps down.
2202
+ */
2203
+ function MessageList({ messages, onRetry, onEdit, onFeedback, renderPart, footer, className }) {
2204
+ const scroller = useRef(null);
2205
+ const content = useRef(null);
2206
+ const stuck = useRef(true);
2207
+ const [atBottom, setAtBottom] = useState(true);
2208
+ const toBottom = useCallback((behavior = "auto") => {
2209
+ const el = scroller.current;
2210
+ if (el) el.scrollTo({
2211
+ top: el.scrollHeight,
2212
+ behavior
2213
+ });
2214
+ }, []);
2215
+ const onScroll = () => {
2216
+ const el = scroller.current;
2217
+ if (!el) return;
2218
+ const bottom = el.scrollHeight - el.scrollTop - el.clientHeight < STICK_SLOP;
2219
+ stuck.current = bottom;
2220
+ setAtBottom(bottom);
2221
+ };
2222
+ useEffect(() => {
2223
+ const el = content.current;
2224
+ if (!el) return;
2225
+ const observer = new ResizeObserver(() => {
2226
+ if (stuck.current) toBottom();
2227
+ });
2228
+ observer.observe(el);
2229
+ return () => observer.disconnect();
2230
+ }, [toBottom]);
2231
+ const lastUser = messages.findLast((m) => m.role === "user")?.id;
2232
+ useLayoutEffect(() => {
2233
+ stuck.current = true;
2234
+ setAtBottom(true);
2235
+ toBottom();
2236
+ }, [lastUser, toBottom]);
2237
+ return /* @__PURE__ */ jsxs("div", {
2238
+ className: clsx("relative min-h-0 flex-1", className),
2239
+ children: [/* @__PURE__ */ jsx("div", {
2240
+ ref: scroller,
2241
+ onScroll,
2242
+ className: "h-full overflow-y-auto [scrollbar-gutter:stable] [scrollbar-width:thin]",
2243
+ children: /* @__PURE__ */ jsxs("div", {
2244
+ ref: content,
2245
+ className: "flex min-h-full flex-col",
2246
+ children: [/* @__PURE__ */ jsx("div", {
2247
+ role: "log",
2248
+ "aria-label": "Conversation",
2249
+ 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",
2250
+ children: messages.map((m) => m.role === "user" ? /* @__PURE__ */ jsx(UserMessage, {
2251
+ message: m,
2252
+ ...onEdit && { onEdit: () => onEdit(m.id) }
2253
+ }, m.id) : /* @__PURE__ */ jsx(AssistantMessage, {
2254
+ message: m,
2255
+ ...onRetry && { onRetry: () => onRetry(m.id) },
2256
+ ...onFeedback && { onFeedback: (f) => onFeedback(m.id, f) },
2257
+ ...renderPart && { renderPart }
2258
+ }, m.id))
2259
+ }), footer && /* @__PURE__ */ jsx("div", {
2260
+ className: "sticky bottom-0",
2261
+ children: footer
2262
+ })]
2263
+ })
2264
+ }), !atBottom && /* @__PURE__ */ jsx(IconButton, {
2265
+ label: "Scroll to bottom",
2266
+ variant: "subtle",
2267
+ onPress: () => {
2268
+ stuck.current = true;
2269
+ toBottom("smooth");
2270
+ },
2271
+ className: "absolute bottom-[min(10rem,40%)] left-1/2 -translate-x-1/2 rounded-full border border-chat-border shadow-chat-popover",
2272
+ children: /* @__PURE__ */ jsx(ArrowDown, {})
2273
+ })]
2274
+ });
2275
+ }
2276
+ // src/components/chat-header.tsx
2277
+ /** The bar above a chat: breadcrumb and title with its menu, actions on the right. */
2278
+ function ChatHeader({ title, menu, leading, actions, className }) {
2279
+ return /* @__PURE__ */ jsxs("header", {
2280
+ className: clsx("flex h-12 shrink-0 items-center gap-1 px-3 font-chat-sans", className),
2281
+ children: [
2282
+ leading,
2283
+ menu ? /* @__PURE__ */ jsxs(MenuTrigger, { children: [/* @__PURE__ */ jsxs(Button, {
2284
+ size: "sm",
2285
+ className: "min-w-0 text-chat-fg",
2286
+ children: [/* @__PURE__ */ jsx("span", {
2287
+ className: "truncate",
2288
+ children: title
2289
+ }), /* @__PURE__ */ jsx(ChevronDown, {
2290
+ "aria-hidden": true,
2291
+ className: "size-3.5 shrink-0 text-chat-muted"
2292
+ })]
2293
+ }), /* @__PURE__ */ jsx(Popover, {
2294
+ placement: "bottom start",
2295
+ children: menu
2296
+ })] }) : /* @__PURE__ */ jsx("h1", {
2297
+ className: "min-w-0 truncate px-2 text-sm text-chat-fg",
2298
+ children: title
2299
+ }),
2300
+ /* @__PURE__ */ jsx("div", {
2301
+ className: "ml-auto flex shrink-0 items-center gap-1",
2302
+ children: actions
2303
+ })
2304
+ ]
2305
+ });
2306
+ }
2307
+ /** Pin, Rename, Add to project (searchable), Share, Delete. Each shows only when its handler is set. */
2308
+ function ChatTitleMenu({ pinned, onPin, onRename, projects, onAddToProject, onCreateProject, onShare, onDelete, children }) {
2309
+ const act = (key) => {
2310
+ if (key === "pin") onPin?.();
2311
+ else if (key === "rename") onRename?.();
2312
+ else if (key === "share") onShare?.();
2313
+ else if (key === "delete") onDelete?.();
2314
+ };
2315
+ return /* @__PURE__ */ jsxs(Menu, {
2316
+ "aria-label": "Chat actions",
2317
+ className: "min-w-44",
2318
+ onAction: act,
2319
+ children: [
2320
+ onPin && /* @__PURE__ */ jsx(MenuItem, {
2321
+ id: "pin",
2322
+ icon: pinned ? /* @__PURE__ */ jsx(PinOff, {}) : /* @__PURE__ */ jsx(Pin, {}),
2323
+ shortcut: "P",
2324
+ children: pinned ? "Unpin" : "Pin"
2325
+ }),
2326
+ onRename && /* @__PURE__ */ jsx(MenuItem, {
2327
+ id: "rename",
2328
+ icon: /* @__PURE__ */ jsx(Pencil, {}),
2329
+ shortcut: "R",
2330
+ children: "Rename"
2331
+ }),
2332
+ onAddToProject && /* @__PURE__ */ jsxs(SubmenuTrigger, { children: [/* @__PURE__ */ jsx(MenuItem, {
2333
+ id: "project",
2334
+ icon: /* @__PURE__ */ jsx(FolderPlus, {}),
2335
+ children: "Add to project"
2336
+ }), /* @__PURE__ */ jsx(Popover, {
2337
+ placement: "end top",
2338
+ className: "w-60",
2339
+ children: /* @__PURE__ */ jsx(MenuSearch, {
2340
+ placeholder: "Search or create a project",
2341
+ children: /* @__PURE__ */ jsxs(Menu, {
2342
+ "aria-label": "Projects",
2343
+ className: "max-h-72",
2344
+ onAction: (key) => key === "__new" ? onCreateProject?.() : onAddToProject(String(key)),
2345
+ children: [onCreateProject && /* @__PURE__ */ jsx(MenuItem, {
2346
+ id: "__new",
2347
+ icon: /* @__PURE__ */ jsx(Plus, {}),
2348
+ children: "New project"
2349
+ }), (projects ?? []).map((p) => /* @__PURE__ */ jsx(MenuItem, {
2350
+ id: p.id,
2351
+ children: p.name
2352
+ }, p.id))]
2353
+ })
2354
+ })
2355
+ })] }),
2356
+ onShare && /* @__PURE__ */ jsx(MenuItem, {
2357
+ id: "share",
2358
+ icon: /* @__PURE__ */ jsx(Share, {}),
2359
+ children: "Share chat"
2360
+ }),
2361
+ children,
2362
+ onDelete && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MenuSeparator, {}), /* @__PURE__ */ jsx(MenuItem, {
2363
+ id: "delete",
2364
+ icon: /* @__PURE__ */ jsx(Trash2, {}),
2365
+ shortcut: "D",
2366
+ tone: "danger",
2367
+ children: "Delete"
2368
+ })] })
2369
+ ]
2370
+ });
2371
+ }
2372
+ // src/components/chat-list.tsx
2373
+ const isProject = (item) => "type" in item && item.type === "project";
2374
+ 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");
2375
+ /**
2376
+ * The chat picker: sections of chats and projects, collapsible, with an active row, running and unread
2377
+ * markers and a per-row menu. Usable on its own (in a host's own sidebar) or inside `Sidebar`.
2378
+ */
2379
+ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }) {
2380
+ const row = (chat, nested = false) => /* @__PURE__ */ jsx(ChatRow, {
2381
+ chat,
2382
+ active: chat.id === activeId,
2383
+ nested,
2384
+ ...onSelect && { onSelect },
2385
+ ...rowMenu && { menu: rowMenu(chat) }
2386
+ }, chat.id);
2387
+ const items = (list) => list.map((item) => isProject(item) ? /* @__PURE__ */ jsx(ProjectRow, {
2388
+ project: item,
2389
+ activeId,
2390
+ row
2391
+ }, item.id) : row(item));
2392
+ return /* @__PURE__ */ jsx("nav", {
2393
+ "aria-label": props["aria-label"] ?? "Chats",
2394
+ className: clsx("flex flex-col gap-3", className),
2395
+ children: sections.map((section) => section.title ? /* @__PURE__ */ jsxs(Disclosure$1, {
2396
+ defaultExpanded: section.defaultExpanded ?? true,
2397
+ className: "group/section",
2398
+ children: [/* @__PURE__ */ jsxs(Heading, {
2399
+ className: "m-0 flex h-7 items-center gap-1 pr-1 pl-2 font-chat-sans text-xs font-normal text-chat-subtle",
2400
+ children: [
2401
+ /* @__PURE__ */ jsxs(Button$1, {
2402
+ slot: "trigger",
2403
+ className: "group/trigger flex min-w-0 cursor-pointer items-center gap-1 rounded-chat-sm outline-none data-hovered:text-chat-muted data-focus-visible:ring-2 data-focus-visible:ring-chat-focus",
2404
+ children: [/* @__PURE__ */ jsx("span", {
2405
+ className: "truncate",
2406
+ children: section.title
2407
+ }), /* @__PURE__ */ jsx(ChevronRight, {
2408
+ "aria-hidden": true,
2409
+ 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"
2410
+ })]
2411
+ }),
2412
+ section.badge,
2413
+ /* @__PURE__ */ jsx("span", {
2414
+ className: "ml-auto flex items-center",
2415
+ children: section.action
2416
+ })
2417
+ ]
2418
+ }), /* @__PURE__ */ jsxs(DisclosurePanel, { children: [/* @__PURE__ */ jsx("ul", {
2419
+ className: "flex flex-col gap-px",
2420
+ children: items(section.items)
2421
+ }), section.onViewAll && /* @__PURE__ */ jsx(Button$1, {
2422
+ onPress: section.onViewAll,
2423
+ className: clsx(rowClass, "text-chat-muted"),
2424
+ children: "View all"
2425
+ })] })]
2426
+ }, section.id) : /* @__PURE__ */ jsx("ul", {
2427
+ className: "flex flex-col gap-px",
2428
+ children: items(section.items)
2429
+ }, section.id))
2430
+ });
2431
+ }
2432
+ function ChatRow({ chat, active, nested, onSelect, menu }) {
2433
+ const [menuOpen, setMenuOpen] = useState(false);
2434
+ const { dismiss } = useSidebar();
2435
+ const markerClass = "size-2 shrink-0 rounded-full";
2436
+ const marker = chat.running ? /* @__PURE__ */ jsx("span", {
2437
+ role: "img",
2438
+ "aria-label": "Running",
2439
+ className: clsx(markerClass, "animate-pulse bg-chat-accent motion-reduce:animate-none")
2440
+ }) : chat.unread ? /* @__PURE__ */ jsx("span", {
2441
+ role: "img",
2442
+ "aria-label": "Unread",
2443
+ className: clsx(markerClass, "bg-chat-check")
2444
+ }) : null;
2445
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2446
+ className: clsx("min-w-0 flex-1 truncate", chat.unread && !active && "font-medium"),
2447
+ children: chat.title
2448
+ }), marker] });
2449
+ const common = {
2450
+ className: clsx(rowClass, nested && "pl-8", active && "bg-chat-selected", menu && (menuOpen || active ? "pr-8" : "group-hover/item:pr-8 group-focus-within/item:pr-8"), menu && "[@media(hover:none)]:pr-10"),
2451
+ "aria-current": active ? "page" : void 0,
2452
+ onPress: () => (onSelect?.(chat.id), dismiss())
2453
+ };
2454
+ return /* @__PURE__ */ jsxs("li", {
2455
+ className: "group/item relative",
2456
+ children: [chat.href ? /* @__PURE__ */ jsx(Link, {
2457
+ ...common,
2458
+ href: chat.href,
2459
+ children: content
2460
+ }) : /* @__PURE__ */ jsx(Button$1, {
2461
+ ...common,
2462
+ children: content
2463
+ }), menu && /* @__PURE__ */ jsxs(MenuTrigger, {
2464
+ isOpen: menuOpen,
2465
+ onOpenChange: setMenuOpen,
2466
+ children: [/* @__PURE__ */ jsx(IconButton, {
2467
+ label: `More options for ${chat.title}`,
2468
+ size: "sm",
2469
+ tooltip: false,
2470
+ 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"),
2471
+ children: /* @__PURE__ */ jsx(MoreHorizontal, {})
2472
+ }), /* @__PURE__ */ jsx(Popover, {
2473
+ placement: "bottom start",
2474
+ children: menu
2475
+ })]
2476
+ })]
2477
+ });
2478
+ }
2479
+ function ProjectRow({ project, activeId, row }) {
2480
+ const holdsActive = project.chats.some((c) => c.id === activeId);
2481
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Disclosure$1, {
2482
+ defaultExpanded: project.defaultExpanded ?? holdsActive,
2483
+ children: ({ isExpanded }) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Heading, {
2484
+ className: "m-0 font-normal",
2485
+ children: /* @__PURE__ */ jsxs(Button$1, {
2486
+ slot: "trigger",
2487
+ className: rowClass,
2488
+ children: [
2489
+ isExpanded ? /* @__PURE__ */ jsx(FolderOpen, {
2490
+ "aria-hidden": true,
2491
+ className: "size-4 shrink-0 text-chat-muted"
2492
+ }) : /* @__PURE__ */ jsx(Folder, {
2493
+ "aria-hidden": true,
2494
+ className: "size-4 shrink-0 text-chat-muted"
2495
+ }),
2496
+ /* @__PURE__ */ jsx("span", {
2497
+ className: "min-w-0 flex-1 truncate",
2498
+ children: project.name
2499
+ }),
2500
+ !isExpanded && project.chats.some((c) => c.unread || c.running) && /* @__PURE__ */ jsx(Badge, { children: "New" })
2501
+ ]
2502
+ })
2503
+ }), /* @__PURE__ */ jsx(DisclosurePanel, { children: /* @__PURE__ */ jsx("ul", {
2504
+ className: "flex flex-col gap-px",
2505
+ children: project.chats.map((c) => row(c, true))
2506
+ }) })] })
2507
+ }) });
2508
+ }
2509
+ // src/views/chat-view.tsx
2510
+ /** Column defaults per density; `contentWidth` / `contentPadding` override them. */
2511
+ const densityVars = {
2512
+ full: {},
2513
+ panel: {
2514
+ "--chat-content-width": "100%",
2515
+ "--chat-content-padding": "1rem",
2516
+ "--chat-message-font-size": "1rem"
2517
+ },
2518
+ inline: {
2519
+ "--chat-content-width": "100%",
2520
+ "--chat-content-padding": "0.75rem",
2521
+ "--chat-message-font-size": "0.9375rem"
2522
+ }
2523
+ };
2524
+ /** A whole chat: header, the conversation, the docked composer. An empty chat shows `emptyState`. */
2525
+ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove, onRetry, onEdit, onFeedback, renderPart, header, composer, emptyState, disclaimer, density = "full", contentWidth, contentPadding, className }) {
2526
+ const style = {
2527
+ ...densityVars[density],
2528
+ ...contentWidth && { "--chat-content-width": contentWidth },
2529
+ ...contentPadding && { "--chat-content-padding": contentPadding }
2530
+ };
2531
+ const empty = messages.length === 0;
2532
+ const [body, short] = useShortPane();
2533
+ const pending = onApprove && approvals?.[0];
2534
+ const box = (variant) => /* @__PURE__ */ jsx(Composer, {
2535
+ variant: density === "inline" || short && !empty ? "compact" : variant,
2536
+ busy,
2537
+ onSubmit: onSend,
2538
+ ...onStop && { onStop },
2539
+ ...composer,
2540
+ ...pending && { placeholder: "Or reply directly…" }
2541
+ });
2542
+ return /* @__PURE__ */ jsxs("div", {
2543
+ "data-chat-density": density,
2544
+ style,
2545
+ className: clsx("chat-view flex h-full min-h-0 flex-col bg-chat-bg text-chat-fg", className),
2546
+ children: [header, /* @__PURE__ */ jsx("div", {
2547
+ ref: body,
2548
+ className: "chat-view-body flex min-h-0 flex-1 flex-col",
2549
+ children: empty ? /* @__PURE__ */ jsx("div", {
2550
+ className: "flex flex-1 flex-col items-center justify-center overflow-y-auto px-(--chat-gutter) pb-[10cqh]",
2551
+ children: /* @__PURE__ */ jsx("div", {
2552
+ className: "w-full max-w-(--chat-content-width)",
2553
+ children: emptyState ? emptyState(box("centered")) : box("centered")
2554
+ })
2555
+ }) : /* @__PURE__ */ jsx(MessageList, {
2556
+ messages,
2557
+ ...onRetry && { onRetry },
2558
+ ...onEdit && { onEdit },
2559
+ ...onFeedback && { onFeedback },
2560
+ ...renderPart && { renderPart },
2561
+ footer: /* @__PURE__ */ jsx("div", {
2562
+ className: clsx("bg-linear-to-t from-chat-bg from-70% to-transparent px-(--chat-gutter)", short ? "pt-2" : "pt-4"),
2563
+ children: /* @__PURE__ */ jsxs("div", {
2564
+ className: "mx-auto w-full max-w-(--chat-content-width)",
2565
+ children: [
2566
+ pending && /* @__PURE__ */ jsx(ApprovalCard, {
2567
+ approval: pending,
2568
+ onRespond: onApprove,
2569
+ className: "mb-2"
2570
+ }, pending.part.id),
2571
+ box("docked"),
2572
+ /* @__PURE__ */ jsx("div", {
2573
+ 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"),
2574
+ children: disclaimer && !short && /* @__PURE__ */ jsx("span", { children: disclaimer })
2575
+ })
2576
+ ]
2577
+ })
2578
+ })
2579
+ })
2580
+ })]
2581
+ });
2582
+ }
2583
+ /** Below this pane height (px) the docked composer is one row and the disclaimer hides, e.g. stacked panes or a phone on its side. */
2584
+ const SHORT_PANE = 360;
2585
+ /** Tracks the chat body's own height, so it adapts to its pane rather than the window. */
2586
+ function useShortPane() {
2587
+ const [short, setShort] = useState(false);
2588
+ const observer = useRef(null);
2589
+ return [useCallback((el) => {
2590
+ observer.current?.disconnect();
2591
+ if (!el) return;
2592
+ observer.current = new ResizeObserver(([entry]) => entry && setShort(entry.contentRect.height < SHORT_PANE));
2593
+ observer.current.observe(el);
2594
+ }, []), short];
2595
+ }
2596
+ /** A new chat: greeting over a centered composer. */
2597
+ function EmptyState({ greeting, icon, children, footer }) {
2598
+ return /* @__PURE__ */ jsxs("div", {
2599
+ className: "flex flex-col gap-8",
2600
+ children: [
2601
+ /* @__PURE__ */ jsxs("h1", {
2602
+ className: "chat-empty-greeting flex items-center justify-center gap-3 text-center font-chat-serif text-[clamp(1.5rem,6cqw,2.5rem)] font-normal text-chat-fg",
2603
+ children: [icon && /* @__PURE__ */ jsx("span", {
2604
+ className: "flex shrink-0 text-chat-accent [&_svg]:size-[1em]",
2605
+ children: icon
2606
+ }), greeting]
2607
+ }),
2608
+ children,
2609
+ footer
2610
+ ]
2611
+ });
2612
+ }
2613
+ // src/views/app-shell.tsx
2614
+ /**
2615
+ * A page with a sidebar: pinned and collapsible to an icon rail from tablet width up, a drawer over the
2616
+ * content on phones. Children get `useSidebar()` and `<SidebarToggle>`.
2617
+ */
2618
+ function AppShell({ sidebar, children, defaultCollapsed, className }) {
2619
+ const drawer = useMediaQuery(DRAWER_QUERY);
2620
+ const [collapsed, setCollapsed] = useState(() => defaultCollapsed ?? (typeof window !== "undefined" && window.matchMedia("(max-width: 1023.98px)").matches));
2621
+ const [drawerOpen, setDrawerOpen] = useState(false);
2622
+ const toggle = useCallback(() => drawer ? setDrawerOpen((o) => !o) : setCollapsed((c) => !c), [drawer]);
2623
+ const dismiss = useCallback(() => setDrawerOpen(false), []);
2624
+ const pinned = useMemo(() => ({
2625
+ collapsed,
2626
+ drawer,
2627
+ drawerOpen,
2628
+ toggle,
2629
+ dismiss
2630
+ }), [
2631
+ collapsed,
2632
+ drawer,
2633
+ drawerOpen,
2634
+ toggle,
2635
+ dismiss
2636
+ ]);
2637
+ const inDrawer = useMemo(() => ({
2638
+ ...pinned,
2639
+ collapsed: false
2640
+ }), [pinned]);
2641
+ return /* @__PURE__ */ jsxs(SidebarContext.Provider, {
2642
+ value: pinned,
2643
+ children: [/* @__PURE__ */ jsxs("div", {
2644
+ className: clsx("flex h-dvh overflow-hidden bg-chat-bg text-chat-fg", className),
2645
+ children: [!drawer && /* @__PURE__ */ jsx("aside", {
2646
+ className: clsx("shrink-0 overflow-hidden border-r border-chat-divider bg-chat-sidebar transition-[width] duration-200 ease-out motion-reduce:transition-none", collapsed ? "w-13" : "w-(--chat-sidebar-width,18rem)"),
2647
+ children: sidebar
2648
+ }), /* @__PURE__ */ jsx("main", {
2649
+ className: "flex min-w-0 flex-1 flex-col",
2650
+ children
2651
+ })]
2652
+ }), drawer && /* @__PURE__ */ jsx(Drawer, {
2653
+ isOpen: drawerOpen,
2654
+ onOpenChange: setDrawerOpen,
2655
+ "aria-label": "Sidebar",
2656
+ children: /* @__PURE__ */ jsx(SidebarContext.Provider, {
2657
+ value: inDrawer,
2658
+ children: sidebar
2659
+ })
2660
+ })]
2661
+ });
2662
+ }
2663
+ /** Opens the drawer on phones, collapses / expands the pinned sidebar elsewhere. */
2664
+ function SidebarToggle({ drawerOnly = false, className }) {
2665
+ const { drawer, drawerOpen, collapsed, toggle } = useSidebar();
2666
+ if (drawerOnly && !drawer) return null;
2667
+ return /* @__PURE__ */ jsx(IconButton, {
2668
+ label: drawer ? drawerOpen ? "Close sidebar" : "Open sidebar" : collapsed ? "Expand sidebar" : "Collapse sidebar",
2669
+ onPress: toggle,
2670
+ className,
2671
+ children: /* @__PURE__ */ jsx(PanelLeft, {})
2672
+ });
2673
+ }
2674
+ // src/views/sidebar.tsx
2675
+ /** The claude.ai sidebar frame. Collapses to an icon rail inside `AppShell`; a drawer on phones. */
2676
+ function Sidebar({ brand, headerActions, nav, children, footer, className }) {
2677
+ const { collapsed } = useSidebar();
2678
+ return /* @__PURE__ */ jsxs("div", {
2679
+ className: clsx("flex h-full flex-col font-chat-sans", collapsed ? "w-13" : "w-full", className),
2680
+ children: [
2681
+ /* @__PURE__ */ jsxs("div", {
2682
+ className: clsx("flex h-12 shrink-0 items-center gap-1 px-2", collapsed && "justify-center"),
2683
+ children: [
2684
+ !collapsed && /* @__PURE__ */ jsx("div", {
2685
+ className: "min-w-0 flex-1 truncate px-1.5 font-chat-serif text-lg text-chat-fg",
2686
+ children: brand
2687
+ }),
2688
+ !collapsed && headerActions,
2689
+ /* @__PURE__ */ jsx(SidebarToggle, {})
2690
+ ]
2691
+ }),
2692
+ nav && /* @__PURE__ */ jsx("ul", {
2693
+ className: clsx("flex shrink-0 flex-col gap-px px-2 pb-2", collapsed && "items-center"),
2694
+ children: nav
2695
+ }),
2696
+ !collapsed && /* @__PURE__ */ jsx("div", {
2697
+ className: "min-h-0 flex-1 overflow-y-auto px-2 pt-2 pb-4 [scrollbar-width:thin]",
2698
+ children
2699
+ }),
2700
+ collapsed && /* @__PURE__ */ jsx("div", { className: "flex-1" }),
2701
+ footer && /* @__PURE__ */ jsx("div", {
2702
+ className: "shrink-0 border-t border-chat-divider p-2 pb-[max(0.5rem,env(safe-area-inset-bottom))]",
2703
+ children: footer
2704
+ })
2705
+ ]
2706
+ });
2707
+ }
2708
+ /** A nav row: icon and label; just the icon with a tooltip when collapsed. Closes the drawer when picked. */
2709
+ function SidebarItem({ icon, label, onPress, href, active, shortcut }) {
2710
+ const { collapsed, dismiss } = useSidebar();
2711
+ 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");
2712
+ const press = () => {
2713
+ onPress?.();
2714
+ dismiss();
2715
+ };
2716
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
2717
+ icon,
2718
+ collapsed ? /* @__PURE__ */ jsx("span", {
2719
+ className: "sr-only",
2720
+ children: label
2721
+ }) : /* @__PURE__ */ jsx("span", {
2722
+ className: "min-w-0 flex-1 truncate",
2723
+ children: label
2724
+ }),
2725
+ !collapsed && shortcut && /* @__PURE__ */ jsx("kbd", {
2726
+ className: "font-chat-sans text-xs text-chat-subtle opacity-0 group-data-hovered:opacity-100",
2727
+ children: shortcut
2728
+ })
2729
+ ] });
2730
+ const common = {
2731
+ className,
2732
+ onPress: press,
2733
+ "aria-current": active ? "page" : void 0
2734
+ };
2735
+ const control = href ? /* @__PURE__ */ jsx(Link, {
2736
+ ...common,
2737
+ href,
2738
+ children: content
2739
+ }) : /* @__PURE__ */ jsx(Button$1, {
2740
+ ...common,
2741
+ children: content
2742
+ });
2743
+ return /* @__PURE__ */ jsx("li", { children: collapsed ? /* @__PURE__ */ jsx(Tooltip, {
2744
+ content: label,
2745
+ placement: "right",
2746
+ children: control
2747
+ }) : control });
2748
+ }
2749
+ const initials = (name) => name.split(/\s+/).slice(0, 2).map((w) => w[0]?.toUpperCase() ?? "").join("");
2750
+ /** The account row at the bottom: avatar, name, plan, and a menu. */
2751
+ function SidebarUser({ name, detail, avatarUrl, menu }) {
2752
+ const { collapsed } = useSidebar();
2753
+ const avatar = avatarUrl ? /* @__PURE__ */ jsx("img", {
2754
+ src: avatarUrl,
2755
+ alt: "",
2756
+ className: "size-8 shrink-0 rounded-full object-cover"
2757
+ }) : /* @__PURE__ */ jsx("span", {
2758
+ "aria-hidden": true,
2759
+ className: "flex size-8 shrink-0 items-center justify-center rounded-full bg-chat-fg text-xs font-medium text-chat-bg",
2760
+ children: initials(name)
2761
+ });
2762
+ const button = /* @__PURE__ */ jsxs(Button$1, {
2763
+ "aria-label": collapsed ? name : void 0,
2764
+ className: clsx("flex h-12 w-full cursor-pointer items-center gap-2.5 rounded-chat-md text-left outline-none", "data-hovered:bg-chat-hover data-pressed:bg-chat-selected data-focus-visible:ring-2 data-focus-visible:ring-chat-focus", collapsed ? "justify-center" : "px-1.5"),
2765
+ children: [avatar, !collapsed && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
2766
+ className: "flex min-w-0 flex-1 flex-col",
2767
+ children: [/* @__PURE__ */ jsx("span", {
2768
+ className: "truncate text-sm text-chat-fg",
2769
+ children: name
2770
+ }), detail && /* @__PURE__ */ jsx("span", {
2771
+ className: "truncate text-xs text-chat-muted",
2772
+ children: detail
2773
+ })]
2774
+ }), menu && /* @__PURE__ */ jsx(ChevronsUpDown, {
2775
+ "aria-hidden": true,
2776
+ className: "size-4 shrink-0 text-chat-muted"
2777
+ })] })]
2778
+ });
2779
+ if (!menu) return button;
2780
+ return /* @__PURE__ */ jsxs(MenuTrigger, { children: [button, /* @__PURE__ */ jsx(Popover, {
2781
+ placement: "top start",
2782
+ children: menu
2783
+ })] });
2784
+ }
2785
+ 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 };